From 503939d6b9f4b9e1ea24038f1e1615bc064840a9 Mon Sep 17 00:00:00 2001 From: Negar Date: Mon, 3 Aug 2026 12:53:55 +0330 Subject: [PATCH] initial commit --- .gitignore | 10 + bank/accounts/__init__.py | 0 bank/accounts/admin.py | 6 + bank/accounts/apps.py | 5 + bank/accounts/migrations/__init__.py | 0 bank/accounts/models.py | 34 ++ bank/accounts/serializers.py | 28 ++ bank/accounts/tests.py | 3 + bank/accounts/urls.py | 10 + bank/accounts/views.py | 292 ++++++++++++++ bank/bank/__init__.py | 0 bank/bank/asgi.py | 16 + bank/bank/serializers.py | 14 + bank/bank/settings.py | 146 +++++++ bank/bank/urls.py | 42 ++ bank/bank/views.py | 100 +++++ bank/bank/wsgi.py | 16 + bank/manage.py | 22 + bank/transactions/__init__.py | 0 bank/transactions/admin.py | 11 + bank/transactions/apps.py | 6 + bank/transactions/migrations/__init__.py | 0 bank/transactions/models.py | 27 ++ bank/transactions/serializers.py | 9 + bank/transactions/tests.py | 3 + bank/transactions/urls.py | 39 ++ bank/transactions/views.py | 485 +++++++++++++++++++++++ 27 files changed, 1324 insertions(+) create mode 100644 .gitignore create mode 100644 bank/accounts/__init__.py create mode 100644 bank/accounts/admin.py create mode 100644 bank/accounts/apps.py create mode 100644 bank/accounts/migrations/__init__.py create mode 100644 bank/accounts/models.py create mode 100644 bank/accounts/serializers.py create mode 100644 bank/accounts/tests.py create mode 100644 bank/accounts/urls.py create mode 100644 bank/accounts/views.py create mode 100644 bank/bank/__init__.py create mode 100644 bank/bank/asgi.py create mode 100644 bank/bank/serializers.py create mode 100644 bank/bank/settings.py create mode 100644 bank/bank/urls.py create mode 100644 bank/bank/views.py create mode 100644 bank/bank/wsgi.py create mode 100644 bank/manage.py create mode 100644 bank/transactions/__init__.py create mode 100644 bank/transactions/admin.py create mode 100644 bank/transactions/apps.py create mode 100644 bank/transactions/migrations/__init__.py create mode 100644 bank/transactions/models.py create mode 100644 bank/transactions/serializers.py create mode 100644 bank/transactions/tests.py create mode 100644 bank/transactions/urls.py create mode 100644 bank/transactions/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..caf1421 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python +__pycache__/ +*.py[cod] + +# Database +**/db.sqlite3 + +# Migrations +**/migrations/*.py +!**/migrations/__init__.py \ No newline at end of file diff --git a/bank/accounts/__init__.py b/bank/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bank/accounts/admin.py b/bank/accounts/admin.py new file mode 100644 index 0000000..fc72a6e --- /dev/null +++ b/bank/accounts/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import Account +@admin.register(Account) +class AccountAdmin(admin.ModelAdmin): + list_display = ('user','account_number','balance') + \ No newline at end of file diff --git a/bank/accounts/apps.py b/bank/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/bank/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/bank/accounts/migrations/__init__.py b/bank/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bank/accounts/models.py b/bank/accounts/models.py new file mode 100644 index 0000000..75e711c --- /dev/null +++ b/bank/accounts/models.py @@ -0,0 +1,34 @@ +from django.db import models +from django.contrib.auth.models import User + +class Account(models.Model): + + ACCOUNT_TYPES = [ + ("gharzolhasaneh", "Gharzolhasaneh"), + ("short_term", "Short Term"), + ("current", "Current"), + ] + + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name="accounts" + ) + + account_number = models.CharField( + max_length=20, + unique=True + ) + + account_type = models.CharField( + max_length=20, + choices=ACCOUNT_TYPES, + default="gharzolhasaneh" + ) + + balance = models.IntegerField( + default=0 + ) + + def __str__(self): + return self.account_number \ No newline at end of file diff --git a/bank/accounts/serializers.py b/bank/accounts/serializers.py new file mode 100644 index 0000000..10c8d68 --- /dev/null +++ b/bank/accounts/serializers.py @@ -0,0 +1,28 @@ +from rest_framework import serializers +from .models import Account + + +class AccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = "__all__" + + +class AccountCreateSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = [ + "account_number", + "account_type" + ] + +class AccountUpdateSerializer(serializers.ModelSerializer): + id = serializers.IntegerField() + + class Meta: + model = Account + fields = [ + "id", + "account_number", + "account_type", + ] \ No newline at end of file diff --git a/bank/accounts/tests.py b/bank/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/bank/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/bank/accounts/urls.py b/bank/accounts/urls.py new file mode 100644 index 0000000..50340e6 --- /dev/null +++ b/bank/accounts/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views import AccountlistAPIView,AccountCreateAPIView,AccountDetailAPIView,AccountUpdateAPIView,AccountDeleteAPIView,AccountBalanceAPIView +urlpatterns =[ + path("list",AccountlistAPIView.as_view()), + path("Create",AccountCreateAPIView.as_view()), + path("detail//",AccountDetailAPIView.as_view(),name="account-detail"), + path("Update/",AccountUpdateAPIView.as_view(),name="account-update"), + path("Delete/",AccountDeleteAPIView.as_view()), + path("Balance/", AccountBalanceAPIView.as_view()), +] \ No newline at end of file diff --git a/bank/accounts/views.py b/bank/accounts/views.py new file mode 100644 index 0000000..c253687 --- /dev/null +++ b/bank/accounts/views.py @@ -0,0 +1,292 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from rest_framework.authentication import TokenAuthentication +from .serializers import AccountSerializer, AccountCreateSerializer,AccountUpdateSerializer +from .models import Account +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status +from rest_framework_simplejwt.authentication import JWTAuthentication +from django.db.models import Sum + +class AccountlistAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + + @swagger_auto_schema( + operation_summary="Get My Accounts", + operation_description="Retrieve current user's accounts.", + responses={ + 200: AccountSerializer(many=True), + }, + tags=["Accounts"], + ) + def get(self, request): + + accounts = Account.objects.filter( + user=request.user + ) + + serializer = AccountSerializer( + accounts, + many=True + ) + + return Response( + serializer.data, + status=status.HTTP_200_OK + ) + +class AccountCreateAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="Create Account", + operation_description="Create a new account.", + request_body=AccountCreateSerializer, + responses={ + 201: AccountSerializer, + 400: "Bad Request", + }, + tags=["Accounts"], + ) + def post(self, request): + + serializer = AccountCreateSerializer(data=request.data) + + if serializer.is_valid(): + + account = serializer.save(user=request.user) + + response_serializer = AccountSerializer(account) + + return Response( + response_serializer.data, + status=status.HTTP_201_CREATED + ) + + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) +class AccountDetailAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + + @swagger_auto_schema( + operation_summary="Get Account", + operation_description="Retrieve current user's account by ID.", + + manual_parameters=[ + openapi.Parameter( + "id", + openapi.IN_PATH, + description="Account ID", + type=openapi.TYPE_INTEGER, + required=True + ) + ], + + responses={ + 200: AccountSerializer, + 404: "Account not found", + }, + + tags=["Accounts"], + ) + def get(self, request, id): + + try: + + account = Account.objects.get( + id=id, + user=request.user + ) + + except Account.DoesNotExist: + + return Response( + { + "message": "Account not found" + }, + status=status.HTTP_404_NOT_FOUND + ) + + + serializer = AccountSerializer(account) + + return Response( + serializer.data, + status=status.HTTP_200_OK + ) + +class AccountUpdateAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="Update Account", + operation_description="Update one of current user's accounts.", + request_body=AccountUpdateSerializer, + responses={ + 200: AccountSerializer, + 400: "Bad Request", + 404: "Account Not Found", + }, + tags=["Accounts"], + ) + def put(self, request): + + account_id = request.data.get("id") + + try: + account = Account.objects.get( + id=account_id, + user=request.user + ) + + except Account.DoesNotExist: + return Response( + {"message": "Account not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + serializer = AccountSerializer( + account, + data=request.data, + partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response( + serializer.data, + status=status.HTTP_200_OK + ) + + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + +class AccountDeleteAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + + @swagger_auto_schema( + operation_summary="Delete Account", + operation_description="Delete an account by id from request body", + + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=[ + "id" + ], + properties={ + "id": openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Account ID", + example=2 + ) + } + ), + + responses={ + 204: openapi.Response( + description="Account deleted successfully" + ), + 404: openapi.Response( + description="Account not found" + ) + }, + + tags=["Accounts"] + ) + def delete(self, request): + + account_id = request.data.get("id") + + try: + account = Account.objects.get( + id=account_id, + user=request.user + ) + + except Account.DoesNotExist: + return Response( + { + "message": "Account not found" + }, + status=status.HTTP_404_NOT_FOUND + ) + + + account.delete() + + return Response( + { + "message": "Account deleted successfully" + }, + status=status.HTTP_202_ACCEPTED + ) + + + +class AccountBalanceAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + + @swagger_auto_schema( + operation_summary="Get Account Balance", + operation_description="Get current user's account balance using JWT token.", + + responses={ + 200: openapi.Response( + description="Account balance retrieved successfully", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "account_number": openapi.Schema( + type=openapi.TYPE_STRING + ), + "balance": openapi.Schema( + type=openapi.TYPE_INTEGER + ), + } + ) + ), + + 404: openapi.Response( + description="Account not found" + ) + }, + + tags=["Accounts"] + ) + + def get(self, request): + + total_balance = ( + Account.objects.filter(user=request.user) + .aggregate(total=Sum("balance"))["total"] or 0 + ) + + return Response( + { + "total_balance": total_balance + }, + status=status.HTTP_200_OK + ) \ No newline at end of file diff --git a/bank/bank/__init__.py b/bank/bank/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bank/bank/asgi.py b/bank/bank/asgi.py new file mode 100644 index 0000000..fb44b06 --- /dev/null +++ b/bank/bank/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for bank project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bank.settings') + +application = get_asgi_application() diff --git a/bank/bank/serializers.py b/bank/bank/serializers.py new file mode 100644 index 0000000..a8af593 --- /dev/null +++ b/bank/bank/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from django.contrib.auth.models import User +from .models import bank + +class bankSerializer(serializers.ModelSerializer): + class meta: + model = bank + fields = '__all__' + +from rest_framework import serializers + +class RegisterSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField(write_only=True) \ No newline at end of file diff --git a/bank/bank/settings.py b/bank/bank/settings.py new file mode 100644 index 0000000..7cac019 --- /dev/null +++ b/bank/bank/settings.py @@ -0,0 +1,146 @@ +""" +Django settings for bank project. + +Generated by 'django-admin startproject' using Django 6.0.7. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.0/ref/settings/ +""" + +from pathlib import Path +from datetime import timedelta + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-l&0%5l^5h7)4z!y&jf5jng@+nuo95l+usfc_qv!8g43b1-k24!' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + "rest_framework", + 'rest_framework_simplejwt', + 'accounts', + 'rest_framework.authtoken', + 'bank', + 'transactions', + 'drf_yasg', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'bank.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'bank.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/6.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = 'static/' + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES':( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), +} +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(days=30), + "REFRESH_TOKEN_LIFETIME": timedelta(days=90), +} + +SWAGGER_SETTINGS = { + "SECURITY_DEFINITIONS": { + "Bearer": { + "type": "apiKey", + "name": "Authorization", + "in": "header", + "description": "JWT Authorization. Example: Bearer " + } + }, +} \ No newline at end of file diff --git a/bank/bank/urls.py b/bank/bank/urls.py new file mode 100644 index 0000000..4092071 --- /dev/null +++ b/bank/bank/urls.py @@ -0,0 +1,42 @@ +""" +URL configuration for bank project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from rest_framework_simplejwt.views import (TokenObtainPairView, TokenRefreshView) +from .views import Register +from django.urls import path,include +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from rest_framework.permissions import AllowAny + +schema_view = get_schema_view( + openapi.Info( + title="Bank API", + default_version="v1", + ), + public=True, + permission_classes=(AllowAny,), +) + +urlpatterns = [ + path('admin/', admin.site.urls), + path('account/',include("accounts.urls")), + path('login/', TokenObtainPairView.as_view()), + path('register/', Register.as_view()), + path('refresh/',TokenRefreshView .as_view()), + path('transaction/',include('transactions.urls')), + path("swagger/",schema_view.with_ui("swagger",cache_timeout=0),), +] diff --git a/bank/bank/views.py b/bank/bank/views.py new file mode 100644 index 0000000..14bad93 --- /dev/null +++ b/bank/bank/views.py @@ -0,0 +1,100 @@ +from django.contrib.auth.models import User +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import serializers +# from drf_spectacular.utils import extend_schema, inline_serializer +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + + +class Register(APIView): + + @swagger_auto_schema( + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=["username", "password"], + properties={ + "username": openapi.Schema( + type=openapi.TYPE_STRING, + description="Username" + ), + "password": openapi.Schema( + type=openapi.TYPE_STRING, + description="Password" + ), + }, + ), + responses={ + 201: "User created", + 409: "Duplicated username", + } + ) + def post(self, request): + body = request.data + username = body['username'] + password = body['password'] + if User.objects.filter(username=username).exists(): + return Response({"massage:duplicated username"}, 409) + User.objects.create_user(username=username, password=password) + return Response({ + "massage":"user created" + },201) + +class login (APIView): + @swagger_auto_schema( + operation_summary="Login", + operation_description="Authenticate a user and return a token.", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=["username", "password"], + properties={ + "username": openapi.Schema( + type=openapi.TYPE_STRING, + description="Username" + ), + "password": openapi.Schema( + type=openapi.TYPE_STRING, + description="Password" + ), + }, + ), + responses={ + 200: openapi.Response( + description="Login successful", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "message": openapi.Schema( + type=openapi.TYPE_STRING, + example="token is : 123456abcdef" + ) + }, + ), + ), + 401: openapi.Response( + description="Invalid username or password", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "message": openapi.Schema( + type=openapi.TYPE_STRING, + example="Invalid username or password" + ) + }, + ), + ), + }, + ) + def post (self,request): + username = request.data.get("username") + password = request.data.get("password") + User = authenticate(username=username,password=password) + if User is None: + return Response ({ + "massage":"token created" + },401) + + token, created = Token.pbjects.get_or_create(User=User) + return Response ({ + "massage":{f"token is :{token}"} + },200) \ No newline at end of file diff --git a/bank/bank/wsgi.py b/bank/bank/wsgi.py new file mode 100644 index 0000000..5c8cee9 --- /dev/null +++ b/bank/bank/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for bank project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bank.settings') + +application = get_wsgi_application() diff --git a/bank/manage.py b/bank/manage.py new file mode 100644 index 0000000..2ebdc79 --- /dev/null +++ b/bank/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bank.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/bank/transactions/__init__.py b/bank/transactions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bank/transactions/admin.py b/bank/transactions/admin.py new file mode 100644 index 0000000..ec15b94 --- /dev/null +++ b/bank/transactions/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from .models import Transaction + +@admin.register(Transaction) +class TransactionAdmin(admin.ModelAdmin): + list_dispiay = ( + "account", + "transaction_type", + "amount", + "created_at", + ) diff --git a/bank/transactions/apps.py b/bank/transactions/apps.py new file mode 100644 index 0000000..be79aab --- /dev/null +++ b/bank/transactions/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TransactionConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'transactions' diff --git a/bank/transactions/migrations/__init__.py b/bank/transactions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bank/transactions/models.py b/bank/transactions/models.py new file mode 100644 index 0000000..94b30e5 --- /dev/null +++ b/bank/transactions/models.py @@ -0,0 +1,27 @@ +from django.db import models + + +class Transaction(models.Model): + + account = models.ForeignKey( + "accounts.Account", + on_delete=models.CASCADE, + related_name="transactions" + ) + + transaction_type = models.CharField( + max_length=20 + ) + + amount = models.DecimalField( + max_digits=12, + decimal_places=2 + ) + + created_at = models.DateTimeField( + auto_now_add=True + ) + + + def __str__(self): + return f"{self.transaction_type} - {self.amount}" \ No newline at end of file diff --git a/bank/transactions/serializers.py b/bank/transactions/serializers.py new file mode 100644 index 0000000..a2d129f --- /dev/null +++ b/bank/transactions/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers +from .models import Transaction + + +class TransactionSerializer(serializers.ModelSerializer): + + class Meta: + model = Transaction + fields = "__all__" \ No newline at end of file diff --git a/bank/transactions/tests.py b/bank/transactions/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/bank/transactions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/bank/transactions/urls.py b/bank/transactions/urls.py new file mode 100644 index 0000000..363db30 --- /dev/null +++ b/bank/transactions/urls.py @@ -0,0 +1,39 @@ +from django.urls import path +from .views import ( + IncreaseAPIView, + DecreaseAPIView, + TransferAPIView, + TransactionListAPIView, + TransactionDetailAPIView +) + + +urlpatterns = [ + + path("increase/", IncreaseAPIView.as_view(), name="increase"), + + path( + "decrease/", + DecreaseAPIView.as_view(), + name="decrease" + ), + + path( + "transfer/", + TransferAPIView.as_view(), + name="transfer" + ), + + path( + "list/", + TransactionListAPIView.as_view(), + name="transaction-list" + ), + + path( + "detail//", + TransactionDetailAPIView.as_view(), + name="transaction-detail" + ), + +] \ No newline at end of file diff --git a/bank/transactions/views.py b/bank/transactions/views.py new file mode 100644 index 0000000..fc2e5e5 --- /dev/null +++ b/bank/transactions/views.py @@ -0,0 +1,485 @@ +from decimal import Decimal + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status + +from rest_framework.permissions import IsAuthenticated +from rest_framework_simplejwt.authentication import JWTAuthentication + +from accounts.models import Account +from .models import Transaction + +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + + + +class IncreaseAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + + @swagger_auto_schema( + operation_summary="Increase Balance", + operation_description="Increase the balance of a selected account", + + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + + required=[ + "account_number", + "amount" + ], + + properties={ + + "account_number": openapi.Schema( + type=openapi.TYPE_STRING, + example="87654321", + description="Account Number" + ), + + "amount": openapi.Schema( + type=openapi.TYPE_INTEGER, + example=5000, + description="Amount to increase" + ) + + } + ), + + responses={ + + 200: openapi.Response( + description="Balance increased successfully", + + examples={ + "application/json": { + "message": "Balance increased successfully", + "balance": 125000 + } + } + ), + + 404: openapi.Response( + description="Account not found", + + examples={ + "application/json": { + "message": "Account not found" + } + } + ) + + }, + + tags=[ + "transaction" + ] + + ) + def post(self, request): + + account_number = request.data["account_number"] + amount = int(request.data["amount"]) + + try: + + account = Account.objects.get( + account_number=account_number, + user=request.user + ) + + except Account.DoesNotExist: + + return Response( + { + "message": "Account not found" + }, + status=status.HTTP_404_NOT_FOUND + ) + + + account.balance += amount + account.save() + + + Transaction.objects.create( + account=account, + transaction_type="increase", + amount=amount + ) + + + return Response( + { + "message": "Balance increased successfully", + "balance": account.balance + }, + status=status.HTTP_200_OK + ) + + +class DecreaseAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + + @swagger_auto_schema( + operation_summary="Decrease Balance", + operation_description="Decrease the balance of a selected account", + + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + + required=[ + "account_number", + "amount" + ], + + properties={ + + "account_number": openapi.Schema( + type=openapi.TYPE_STRING, + example="87654321", + description="Account Number" + ), + + "amount": openapi.Schema( + type=openapi.TYPE_INTEGER, + example=5000, + description="Amount to decrease" + ) + + } + ), + + responses={ + + 200: openapi.Response( + description="Balance decreased successfully", + + examples={ + "application/json": { + "message": "Balance decreased successfully", + "balance": 120000 + } + } + ), + + 400: openapi.Response( + description="Insufficient balance" + ), + + 404: openapi.Response( + description="Account not found" + ) + + }, + + tags=["transaction"] + + ) + def post(self, request): + + account_number = request.data["account_number"] + amount = int(request.data["amount"]) + + try: + + account = Account.objects.get( + account_number=account_number, + user=request.user + ) + + except Account.DoesNotExist: + + return Response( + { + "message": "Account not found" + }, + status=status.HTTP_404_NOT_FOUND + ) + + + if account.balance < amount: + + return Response( + { + "message": "Insufficient balance" + }, + status=status.HTTP_400_BAD_REQUEST + ) + + + account.balance -= amount + account.save() + + + Transaction.objects.create( + account=account, + transaction_type="decrease", + amount=amount + ) + + + return Response( + { + "message": "Balance decreased successfully", + "balance": account.balance + }, + status=status.HTTP_200_OK + ) + + +class TransferAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_summary="Transfer Money", + operation_description="Transfer money from one of your accounts to another account", + + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=[ + "source_account_number", + "destination_account_number", + "amount" + ], + properties={ + "source_account_number": openapi.Schema( + type=openapi.TYPE_STRING, + example="12345678", + description="Source account number" + ), + "destination_account_number": openapi.Schema( + type=openapi.TYPE_STRING, + example="87654321", + description="Destination account number" + ), + "amount": openapi.Schema( + type=openapi.TYPE_INTEGER, + example=5000, + description="Transfer amount" + ) + } + ), + + responses={ + 200: openapi.Response( + description="Transfer successful" + ), + 400: openapi.Response( + description="Insufficient balance" + ), + 404: openapi.Response( + description="Account not found" + ) + }, + + tags=["transaction"] + ) + def post(self, request): + + amount = int(request.data["amount"]) + source_account_number = request.data["source_account_number"] + destination_account_number = request.data["destination_account_number"] + + try: + source_account = Account.objects.get( + account_number=source_account_number, + user=request.user + ) + except Account.DoesNotExist: + return Response( + { + "message": "Source account not found" + }, + status=status.HTTP_404_NOT_FOUND + ) + + try: + destination_account = Account.objects.get( + account_number=destination_account_number + ) + except Account.DoesNotExist: + return Response( + { + "message": "Destination account not found" + }, + status=status.HTTP_404_NOT_FOUND + ) + + if source_account.id == destination_account.id: + return Response( + { + "message": "Source and destination accounts cannot be the same" + }, + status=status.HTTP_400_BAD_REQUEST + ) + + if source_account.balance < amount: + return Response( + { + "message": "Insufficient balance" + }, + status=status.HTTP_400_BAD_REQUEST + ) + + source_account.balance -= amount + destination_account.balance += amount + + source_account.save() + destination_account.save() + + Transaction.objects.create( + account=source_account, + transaction_type="transfer", + amount=amount + ) + + return Response( + { + "message": "Transfer successful", + "balance": source_account.balance + }, + status=status.HTTP_200_OK + ) + + +class TransactionListAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + + @swagger_auto_schema( + operation_summary="Get Transactions", + operation_description="Get all transactions or filter by account number", + manual_parameters=[ + openapi.Parameter( + "account_number", + openapi.IN_QUERY, + description="Account Number (optional)", + type=openapi.TYPE_STRING, + required=False + ) + ], + responses={ + 200: openapi.Response( + description="Transaction list" + ), + 404: openapi.Response( + description="Account not found" + ) + }, + tags=["transaction"] + ) + def get(self, request): + + account_number = request.query_params.get("account_number") + + if account_number: + + try: + account = Account.objects.get( + account_number=account_number, + user=request.user + ) + + except Account.DoesNotExist: + + return Response( + { + "message": "Account not found" + }, + status=status.HTTP_404_NOT_FOUND + ) + + transactions = Transaction.objects.filter( + account=account + ) + + else: + + accounts = Account.objects.filter( + user=request.user + ) + + transactions = Transaction.objects.filter( + account__in=accounts + ) + + + data = [] + + for transaction in transactions: + + data.append( + { + "id": transaction.id, + "account_number": transaction.account.account_number, + "type": transaction.transaction_type, + "amount": transaction.amount, + "date": transaction.created_at + } + ) + + return Response( + data, + status=status.HTTP_200_OK + ) + + +class TransactionDetailAPIView(APIView): + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + + @swagger_auto_schema( + operation_summary="Get Transaction Detail", + operation_description="Get one transaction by id", + responses={ + 200: openapi.Response( + description="Transaction detail" + ), + 404: openapi.Response( + description="Transaction not found" + ) + }, + tags=["transaction"] + ) + def get(self, request, id): + + try: + + transaction = Transaction.objects.get( + id=id, + account__user=request.user + ) + + except Transaction.DoesNotExist: + + return Response( + { + "message": "Transaction not found" + }, + status=status.HTTP_404_NOT_FOUND + ) + + + return Response( + { + "id": transaction.id, + "type": transaction.transaction_type, + "amount": transaction.amount, + "date": transaction.created_at + }, + status=status.HTTP_200_OK + ) \ No newline at end of file