├── core ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── admin.py ├── tests.py ├── apps.py └── views.py ├── accounts ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── constants.py ├── admin.py ├── urls.py ├── managers.py ├── views.py ├── forms.py └── models.py ├── transactions ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── admin.py ├── constants.py ├── urls.py ├── models.py ├── tasks.py ├── forms.py └── views.py ├── requirements.txt ├── banking_system ├── __init__.py ├── asgi.py ├── wsgi.py ├── celery.py ├── urls.py └── settings.py ├── templates ├── core │ ├── footer.html │ ├── messages.html │ ├── base.html │ ├── index.html │ └── navbar.html ├── transactions │ ├── transaction_form.html │ └── transaction_report.html └── accounts │ ├── user_login.html │ └── user_registration.html ├── .github ├── dependabot.yml └── workflows │ ├── changelog-ci.yml │ └── action-updater.yaml ├── manage.py ├── LICENSE ├── CHANGELOG.md ├── .gitignore └── README.md /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transactions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /transactions/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /transactions/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | celery==4.4.7 2 | Django==3.2.9 3 | django-celery-beat==2.1.0 4 | python-dateutil==2.8.2 5 | redis==3.5.3 6 | -------------------------------------------------------------------------------- /accounts/constants.py: -------------------------------------------------------------------------------- 1 | MALE = 'M' 2 | FEMALE = 'F' 3 | 4 | GENDER_CHOICE = ( 5 | (MALE, "Male"), 6 | (FEMALE, "Female"), 7 | ) 8 | -------------------------------------------------------------------------------- /transactions/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TransactionsConfig(AppConfig): 5 | name = 'transactions' 6 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class HomeView(TemplateView): 5 | template_name = 'core/index.html' 6 | -------------------------------------------------------------------------------- /transactions/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from transactions.models import Transaction 4 | 5 | admin.site.register(Transaction) 6 | -------------------------------------------------------------------------------- /banking_system/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from .celery import app as celery_app 4 | 5 | __all__ = ('celery_app',) 6 | -------------------------------------------------------------------------------- /transactions/constants.py: -------------------------------------------------------------------------------- 1 | DEPOSIT = 1 2 | WITHDRAWAL = 2 3 | INTEREST = 3 4 | 5 | TRANSACTION_TYPE_CHOICES = ( 6 | (DEPOSIT, 'Deposit'), 7 | (WITHDRAWAL, 'Withdrawal'), 8 | (INTEREST, 'Interest'), 9 | ) 10 | -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import BankAccountType, User, UserAddress, UserBankAccount 4 | 5 | 6 | admin.site.register(BankAccountType) 7 | admin.site.register(User) 8 | admin.site.register(UserAddress) 9 | admin.site.register(UserBankAccount) 10 | -------------------------------------------------------------------------------- /transactions/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import DepositMoneyView, WithdrawMoneyView, TransactionRepostView 4 | 5 | 6 | app_name = 'transactions' 7 | 8 | 9 | urlpatterns = [ 10 | path("deposit/", DepositMoneyView.as_view(), name="deposit_money"), 11 | path("report/", TransactionRepostView.as_view(), name="transaction_report"), 12 | path("withdraw/", WithdrawMoneyView.as_view(), name="withdraw_money"), 13 | ] 14 | -------------------------------------------------------------------------------- /banking_system/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for banking_system project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'banking_system.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /banking_system/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for banking_system project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'banking_system.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import UserRegistrationView, LogoutView, UserLoginView 4 | 5 | 6 | app_name = 'accounts' 7 | 8 | urlpatterns = [ 9 | path( 10 | "login/", UserLoginView.as_view(), 11 | name="user_login" 12 | ), 13 | path( 14 | "logout/", LogoutView.as_view(), 15 | name="user_logout" 16 | ), 17 | path( 18 | "register/", UserRegistrationView.as_view(), 19 | name="user_registration" 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /templates/core/footer.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/changelog-ci.yml: -------------------------------------------------------------------------------- 1 | name: Changelog-CI 2 | 3 | # Controls when the action will run. Triggers the workflow on pull request 4 | on: 5 | pull_request: 6 | types: [opened, reopened] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | # Checks-out your repository 14 | - uses: actions/checkout@v2 15 | 16 | - name: Run Changelog-CI 17 | uses: saadmk11/changelog-ci@v1.1.0 18 | env: 19 | USERNAME: ${{secrets.USERNAME}} 20 | EMAIL: ${{secrets.EMAIL}} 21 | -------------------------------------------------------------------------------- /templates/core/messages.html: -------------------------------------------------------------------------------- 1 | {% for message in messages %} 2 | 11 | {% endfor %} 12 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'banking_system.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /transactions/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .constants import TRANSACTION_TYPE_CHOICES 4 | from accounts.models import UserBankAccount 5 | 6 | 7 | class Transaction(models.Model): 8 | account = models.ForeignKey( 9 | UserBankAccount, 10 | related_name='transactions', 11 | on_delete=models.CASCADE, 12 | ) 13 | amount = models.DecimalField( 14 | decimal_places=2, 15 | max_digits=12 16 | ) 17 | balance_after_transaction = models.DecimalField( 18 | decimal_places=2, 19 | max_digits=12 20 | ) 21 | transaction_type = models.PositiveSmallIntegerField( 22 | choices=TRANSACTION_TYPE_CHOICES 23 | ) 24 | timestamp = models.DateTimeField(auto_now_add=True) 25 | 26 | def __str__(self): 27 | return str(self.account.account_no) 28 | 29 | class Meta: 30 | ordering = ['timestamp'] 31 | -------------------------------------------------------------------------------- /.github/workflows/action-updater.yaml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Version Updater 2 | 3 | # Controls when the action will run. 4 | on: 5 | # can be used to run workflow manually 6 | workflow_dispatch: 7 | schedule: 8 | # Automatically run on every Sunday 9 | - cron: '0 0 * * 0' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | # Access token with `workflow` scope is required 19 | token: ${{ secrets.WORKFLOW_SECRET }} 20 | 21 | - name: Run GitHub Actions Version Updater 22 | uses: saadmk11/github-actions-version-updater@v0.7.1 23 | with: 24 | # Access token with `workflow` scope is required 25 | token: ${{ secrets.WORKFLOW_SECRET }} 26 | # Do not update these actions (Optional) 27 | # You need to add JSON array inside a string 28 | # because GitHub Actions does not yet allow `Lists` as input 29 | ignore: '["actions/checkout@v2"]' 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Maksudul Haque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /banking_system/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | 5 | from celery import Celery 6 | from celery.schedules import crontab 7 | 8 | # set the default Django settings module for the 'celery' program. 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'banking_system.settings') 10 | 11 | app = Celery('banking_system') 12 | 13 | # Using a string here means the worker doesn't have to serialize 14 | # the configuration object to child processes. 15 | # - namespace='CELERY' means all celery-related configuration keys 16 | # should have a `CELERY_` prefix. 17 | app.config_from_object('django.conf:settings', namespace='CELERY') 18 | 19 | # Load task modules from all registered Django app configs. 20 | app.autodiscover_tasks() 21 | 22 | app.conf.beat_schedule = { 23 | 'calculate_interest': { 24 | 'task': 'calculate_interest', 25 | # http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html 26 | 'schedule': crontab(0, 0, day_of_month='1'), 27 | } 28 | } 29 | 30 | 31 | @app.task(bind=True) 32 | def debug_task(self): 33 | print('Request: {0!r}'.format(self.request)) 34 | -------------------------------------------------------------------------------- /banking_system/urls.py: -------------------------------------------------------------------------------- 1 | """banking_system URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | 19 | from core.views import HomeView 20 | 21 | 22 | urlpatterns = [ 23 | path('', HomeView.as_view(), name='home'), 24 | path('accounts/', include('accounts.urls', namespace='accounts')), 25 | path('admin/', admin.site.urls), 26 | path( 27 | 'transactions/', 28 | include('transactions.urls', namespace='transactions') 29 | ) 30 | ] 31 | -------------------------------------------------------------------------------- /templates/core/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block head_title %}Banking System{% endblock %} 11 | 12 | 13 | 14 | {% block head_extra %}{% endblock %} 15 | 16 | 17 | {% block body %} 18 | {% include 'core/navbar.html' %} 19 |
20 | {% include 'core/messages.html' %} 21 | {% block content %} 22 | 23 | {% endblock %} 24 |
25 | {% include 'core/footer.html' %} 26 | 27 | {% endblock %} 28 | {% block footer_extra %}{% endblock %} 29 | 30 | 31 | -------------------------------------------------------------------------------- /transactions/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-09-01 16:27 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('accounts', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Transaction', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('amount', models.DecimalField(decimal_places=2, max_digits=12)), 21 | ('balance_after_transaction', models.DecimalField(decimal_places=2, max_digits=12)), 22 | ('transaction_type', models.PositiveSmallIntegerField(choices=[(1, 'Deposit'), (2, 'Withdrawal'), (3, 'Interest')])), 23 | ('timestamp', models.DateTimeField(auto_now_add=True)), 24 | ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='accounts.userbankaccount')), 25 | ], 26 | options={ 27 | 'ordering': ['timestamp'], 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /templates/transactions/transaction_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'core/base.html' %} 2 | 3 | {% block head_title %}{{ title }}{% endblock %} 4 | 5 | {% block content %} 6 |

{{ title }}

7 |
8 |
9 |
10 | {% csrf_token %} 11 | 12 |
13 | 16 | 17 |
18 | {% if form.amount.errors %} 19 | {% for error in form.amount.errors %} 20 |

{{ error }}

21 | {% endfor %} 22 | {% endif %} 23 | 26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/core/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'core/base.html' %} 2 | 3 | {% block head_title %}Banking System{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 |
9 |

Banking System

10 |

11 | This is an Example web application created using django. 12 | You can find the sourse code on GitHub 13 |

14 |
15 | Register 16 | GitHub 17 |
18 |
19 | 20 |
21 |

$$$

22 |
23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /transactions/tasks.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | 3 | from celery.decorators import task 4 | 5 | from accounts.models import UserBankAccount 6 | from transactions.constants import INTEREST 7 | from transactions.models import Transaction 8 | 9 | 10 | @task(name="calculate_interest") 11 | def calculate_interest(): 12 | accounts = UserBankAccount.objects.filter( 13 | balance__gt=0, 14 | interest_start_date__gte=timezone.now(), 15 | initial_deposit_date__isnull=False 16 | ).select_related('account_type') 17 | 18 | this_month = timezone.now().month 19 | 20 | created_transactions = [] 21 | updated_accounts = [] 22 | 23 | for account in accounts: 24 | if this_month in account.get_interest_calculation_months(): 25 | interest = account.account_type.calculate_interest( 26 | account.balance 27 | ) 28 | account.balance += interest 29 | account.save() 30 | 31 | transaction_obj = Transaction( 32 | account=account, 33 | transaction_type=INTEREST, 34 | amount=interest 35 | ) 36 | created_transactions.append(transaction_obj) 37 | updated_accounts.append(account) 38 | 39 | if created_transactions: 40 | Transaction.objects.bulk_create(created_transactions) 41 | 42 | if updated_accounts: 43 | UserBankAccount.objects.bulk_update( 44 | updated_accounts, ['balance'] 45 | ) 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version: 2.0.2 2 | 3 | * [#46](https://github.com/saadmk11/banking-system/pull/46): Bump django from 3.1.8 to 3.1.9 4 | * [#22](https://github.com/saadmk11/banking-system/pull/22): Bump django from 3.1.1 to 3.1.2 5 | * [#26](https://github.com/saadmk11/banking-system/pull/26): Bump django from 3.1.2 to 3.1.3 6 | * [#28](https://github.com/saadmk11/banking-system/pull/28): Bump django from 3.1.3 to 3.1.4 7 | * [#24](https://github.com/saadmk11/banking-system/pull/24): Bump django-celery-beat from 2.0.0 to 2.1.0 8 | * [#31](https://github.com/saadmk11/banking-system/pull/31): Bump django from 3.1.4 to 3.1.5 9 | * [#34](https://github.com/saadmk11/banking-system/pull/34): Bump django from 3.1.5 to 3.1.7 10 | * [#37](https://github.com/saadmk11/banking-system/pull/37): Bump django from 3.1.7 to 3.1.8 11 | * [#40](https://github.com/saadmk11/banking-system/pull/40): Update GitHub Action Versions 12 | * [#38](https://github.com/saadmk11/banking-system/pull/38): Add GitHub Actions Version Updater 13 | * [#42](https://github.com/saadmk11/banking-system/pull/42): Update GitHub Action Versions 14 | * [#39](https://github.com/saadmk11/banking-system/pull/39): Update GitHub Action Versions 15 | * [#55](https://github.com/saadmk11/banking-system/pull/55): fixed page not found after login by registered user 16 | * [#54](https://github.com/saadmk11/banking-system/pull/54): Bump django from 3.1.9 to 3.2.7 17 | * [#52](https://github.com/saadmk11/banking-system/pull/52): Bump python-dateutil from 2.8.1 to 2.8.2 18 | 19 | 20 | Version: 2.0.1 21 | ============== 22 | 23 | * [#17](https://github.com/saadmk11/banking-system/pull/17): Create LICENSE 24 | * [#16](https://github.com/saadmk11/banking-system/pull/16): Bump django from 3.1 to 3.1.1 25 | * [#18](https://github.com/saadmk11/banking-system/pull/18): Add Changelog-ci 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | local/ 3 | bin/ 4 | include/ 5 | pip-selfcheck.json 6 | 7 | course_import.py 8 | .idea/ 9 | dist/ 10 | .vscode/ 11 | db.json 12 | htmlcov/ 13 | media/ 14 | db.sqlite3 15 | staticfiles/ 16 | .env 17 | .coverage 18 | celerybeat.pid 19 | celerybeat-schedule 20 | 21 | # Byte-compiled / optimized / DLL files 22 | __pycache__/ 23 | *.py[cod] 24 | *$py.class 25 | 26 | # C extensions 27 | *.so 28 | 29 | # Distribution / packaging 30 | .Python 31 | env/ 32 | build/ 33 | develop-eggs/ 34 | downloads/ 35 | eggs/ 36 | .eggs/ 37 | lib/ 38 | lib64/ 39 | parts/ 40 | sdist/ 41 | var/ 42 | wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | .hypothesis/ 67 | 68 | # Translations 69 | *.mo 70 | *.pot 71 | 72 | # Django stuff: 73 | *.log 74 | local_settings.py 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # dotenv 102 | .env 103 | 104 | # virtualenv 105 | .venv 106 | venv/ 107 | ENV/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | -------------------------------------------------------------------------------- /templates/accounts/user_login.html: -------------------------------------------------------------------------------- 1 | {% extends 'core/base.html' %} 2 | 3 | {% block head_title %}Banking System{% endblock %} 4 | 5 | {% block content %} 6 | {% if form.non_field_errors %} 7 | {% for error in form.non_field_errors %} 8 | 12 | {% endfor %} 13 | {% endif %} 14 |

Sign In

15 |
16 |
17 |
18 | {% csrf_token %} 19 | {% for hidden_field in form.hidden_fields %} 20 | {{ hidden_field.errors }} 21 | {{ hidden_field }} 22 | {% endfor %} 23 | {% for field in form.visible_fields %} 24 |
25 | 28 | 29 |
30 | {% if field.errors %} 31 | {% for error in field.errors %} 32 |

{{ error }}

33 | {% endfor %} 34 | {% endif %} 35 | {% endfor %} 36 | 37 |
38 | 41 |
42 |
43 |
44 | {% endblock %} 45 | 46 | -------------------------------------------------------------------------------- /templates/core/navbar.html: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /accounts/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib import auth 2 | from django.contrib.auth.base_user import BaseUserManager 3 | 4 | 5 | class UserManager(BaseUserManager): 6 | use_in_migrations = True 7 | 8 | def _create_user(self, email, password, **extra_fields): 9 | """ 10 | Create and save a user with the given email, and password. 11 | """ 12 | if not email: 13 | raise ValueError('The given email must be set') 14 | email = self.normalize_email(email) 15 | user = self.model(email=email, **extra_fields) 16 | user.set_password(password) 17 | user.save(using=self._db) 18 | return user 19 | 20 | def create_user(self, email=None, password=None, **extra_fields): 21 | extra_fields.setdefault('is_staff', False) 22 | extra_fields.setdefault('is_superuser', False) 23 | return self._create_user(email, password, **extra_fields) 24 | 25 | def create_superuser(self, email, password=None, **extra_fields): 26 | extra_fields.setdefault('is_staff', True) 27 | extra_fields.setdefault('is_superuser', True) 28 | 29 | if extra_fields.get('is_staff') is not True: 30 | raise ValueError('Superuser must have is_staff=True.') 31 | if extra_fields.get('is_superuser') is not True: 32 | raise ValueError('Superuser must have is_superuser=True.') 33 | 34 | return self._create_user(email, password, **extra_fields) 35 | 36 | def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None): 37 | if backend is None: 38 | backends = auth._get_backends(return_tuples=True) 39 | if len(backends) == 1: 40 | backend, _ = backends[0] 41 | else: 42 | raise ValueError( 43 | 'You have multiple authentication backends configured and ' 44 | 'therefore must provide the `backend` argument.' 45 | ) 46 | elif not isinstance(backend, str): 47 | raise TypeError( 48 | 'backend must be a dotted import path string (got %r).' 49 | % backend 50 | ) 51 | else: 52 | backend = auth.load_backend(backend) 53 | if hasattr(backend, 'with_perm'): 54 | return backend.with_perm( 55 | perm, 56 | is_active=is_active, 57 | include_superusers=include_superusers, 58 | obj=obj, 59 | ) 60 | return self.none() 61 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import get_user_model, login, logout 3 | from django.contrib.auth.views import LoginView 4 | from django.shortcuts import HttpResponseRedirect 5 | from django.urls import reverse_lazy 6 | from django.views.generic import TemplateView, RedirectView 7 | 8 | from .forms import UserRegistrationForm, UserAddressForm 9 | 10 | 11 | User = get_user_model() 12 | 13 | 14 | class UserRegistrationView(TemplateView): 15 | model = User 16 | form_class = UserRegistrationForm 17 | template_name = 'accounts/user_registration.html' 18 | 19 | def dispatch(self, request, *args, **kwargs): 20 | if self.request.user.is_authenticated: 21 | return HttpResponseRedirect( 22 | reverse_lazy('transactions:transaction_report') 23 | ) 24 | return super().dispatch(request, *args, **kwargs) 25 | 26 | def post(self, request, *args, **kwargs): 27 | registration_form = UserRegistrationForm(self.request.POST) 28 | address_form = UserAddressForm(self.request.POST) 29 | 30 | if registration_form.is_valid() and address_form.is_valid(): 31 | user = registration_form.save() 32 | address = address_form.save(commit=False) 33 | address.user = user 34 | address.save() 35 | 36 | login(self.request, user) 37 | messages.success( 38 | self.request, 39 | ( 40 | f'Thank You For Creating A Bank Account. ' 41 | f'Your Account Number is {user.account.account_no}. ' 42 | ) 43 | ) 44 | return HttpResponseRedirect( 45 | reverse_lazy('transactions:deposit_money') 46 | ) 47 | 48 | return self.render_to_response( 49 | self.get_context_data( 50 | registration_form=registration_form, 51 | address_form=address_form 52 | ) 53 | ) 54 | 55 | def get_context_data(self, **kwargs): 56 | if 'registration_form' not in kwargs: 57 | kwargs['registration_form'] = UserRegistrationForm() 58 | if 'address_form' not in kwargs: 59 | kwargs['address_form'] = UserAddressForm() 60 | 61 | return super().get_context_data(**kwargs) 62 | 63 | 64 | class UserLoginView(LoginView): 65 | template_name='accounts/user_login.html' 66 | redirect_authenticated_user = True 67 | 68 | 69 | class LogoutView(RedirectView): 70 | pattern_name = 'home' 71 | 72 | def get_redirect_url(self, *args, **kwargs): 73 | if self.request.user.is_authenticated: 74 | logout(self.request) 75 | return super().get_redirect_url(*args, **kwargs) 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Online Banking System V2.0.2 2 | 3 | This is an Online Banking Concept created using Django Web Framework. 4 | 5 | 6 | ## Features 7 | 8 | * Create Bank Account. 9 | * Deposit & Withdraw Money 10 | * Bank Account Type Support (e.g. Current Account, Savings Account) 11 | * Interest calculation depending on the Bank Account type 12 | * Transaction report with a date range filter 13 | * See balance after every transaction in the Transaction Report 14 | * Calculate Monthly Interest Using Celery Scheduled tasks 15 | * More efficient and accurate interest calculation and balance update 16 | * Ability to add Minimum and Maximum Transaction amount restriction 17 | * Modern UI with Tailwind CSS 18 | 19 | 20 | ## Prerequisites 21 | 22 | Be sure you have the following installed on your development machine: 23 | 24 | + Python >= 3.7 25 | + Redis Server 26 | + Git 27 | + pip 28 | + Virtualenv (virtualenvwrapper is recommended) 29 | 30 | ## Requirements 31 | 32 | + celery==4.4.7 33 | + Django==3.2 34 | + django-celery-beat==2.0.0 35 | + python-dateutil==2.8.1 36 | + redis==3.5.3 37 | 38 | ## Install Redis Server 39 | 40 | [Redis Quick Start](https://redis.io/topics/quickstart) 41 | 42 | Run Redis server 43 | ```bash 44 | redis-server 45 | ``` 46 | 47 | ## Project Installation 48 | 49 | To setup a local development environment: 50 | 51 | Create a virtual environment in which to install Python pip packages. With [virtualenv](https://pypi.python.org/pypi/virtualenv), 52 | ```bash 53 | virtualenv venv # create a virtualenv 54 | source venv/bin/activate # activate the Python virtualenv 55 | ``` 56 | 57 | or with [virtualenvwrapper](http://virtualenvwrapper.readthedocs.org/en/latest/), 58 | ```bash 59 | mkvirtualenv -p python3 {{project_name}} # create and activate environment 60 | workon {{project_name}} # reactivate existing environment 61 | ``` 62 | 63 | Clone GitHub Project, 64 | ```bash 65 | git@github.com:saadmk11/banking-system.git 66 | 67 | cd banking-system 68 | ``` 69 | 70 | Install development dependencies, 71 | ```bash 72 | pip install -r requirements.txt 73 | ``` 74 | 75 | Migrate Database, 76 | ```bash 77 | python manage.py migrate 78 | ``` 79 | 80 | Run the web application locally, 81 | ```bash 82 | python manage.py runserver # 127.0.0.1:8000 83 | ``` 84 | 85 | Create Superuser, 86 | ```bash 87 | python manage.py createsuperuser 88 | ``` 89 | 90 | Run Celery 91 | (Different Terminal Window with Virtual Environment Activated) 92 | ```bash 93 | celery -A banking_system worker -l info 94 | 95 | celery -A banking_system beat -l info 96 | ``` 97 | 98 | ## Images: 99 | ![alt text](https://i.imgur.com/FvgmEJL.png) 100 | # 101 | ![alt text](https://i.imgur.com/aWzj44Y.png) 102 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf import settings 3 | from django.contrib.auth.forms import UserCreationForm 4 | from django.db import transaction 5 | 6 | from .models import User, BankAccountType, UserBankAccount, UserAddress 7 | from .constants import GENDER_CHOICE 8 | 9 | 10 | class UserAddressForm(forms.ModelForm): 11 | 12 | class Meta: 13 | model = UserAddress 14 | fields = [ 15 | 'street_address', 16 | 'city', 17 | 'postal_code', 18 | 'country' 19 | ] 20 | 21 | def __init__(self, *args, **kwargs): 22 | super().__init__(*args, **kwargs) 23 | 24 | for field in self.fields: 25 | self.fields[field].widget.attrs.update({ 26 | 'class': ( 27 | 'appearance-none block w-full bg-gray-200 ' 28 | 'text-gray-700 border border-gray-200 rounded ' 29 | 'py-3 px-4 leading-tight focus:outline-none ' 30 | 'focus:bg-white focus:border-gray-500' 31 | ) 32 | }) 33 | 34 | 35 | class UserRegistrationForm(UserCreationForm): 36 | account_type = forms.ModelChoiceField( 37 | queryset=BankAccountType.objects.all() 38 | ) 39 | gender = forms.ChoiceField(choices=GENDER_CHOICE) 40 | birth_date = forms.DateField() 41 | 42 | class Meta: 43 | model = User 44 | fields = [ 45 | 'first_name', 46 | 'last_name', 47 | 'email', 48 | 'password1', 49 | 'password2', 50 | ] 51 | 52 | def __init__(self, *args, **kwargs): 53 | super().__init__(*args, **kwargs) 54 | 55 | for field in self.fields: 56 | self.fields[field].widget.attrs.update({ 57 | 'class': ( 58 | 'appearance-none block w-full bg-gray-200 ' 59 | 'text-gray-700 border border-gray-200 ' 60 | 'rounded py-3 px-4 leading-tight ' 61 | 'focus:outline-none focus:bg-white ' 62 | 'focus:border-gray-500' 63 | ) 64 | }) 65 | 66 | @transaction.atomic 67 | def save(self, commit=True): 68 | user = super().save(commit=False) 69 | user.set_password(self.cleaned_data["password1"]) 70 | if commit: 71 | user.save() 72 | account_type = self.cleaned_data.get('account_type') 73 | gender = self.cleaned_data.get('gender') 74 | birth_date = self.cleaned_data.get('birth_date') 75 | 76 | UserBankAccount.objects.create( 77 | user=user, 78 | gender=gender, 79 | birth_date=birth_date, 80 | account_type=account_type, 81 | account_no=( 82 | user.id + 83 | settings.ACCOUNT_NUMBER_START_FROM 84 | ) 85 | ) 86 | return user 87 | -------------------------------------------------------------------------------- /transactions/forms.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django import forms 4 | from django.conf import settings 5 | 6 | from .models import Transaction 7 | 8 | 9 | class TransactionForm(forms.ModelForm): 10 | 11 | class Meta: 12 | model = Transaction 13 | fields = [ 14 | 'amount', 15 | 'transaction_type' 16 | ] 17 | 18 | def __init__(self, *args, **kwargs): 19 | self.account = kwargs.pop('account') 20 | super().__init__(*args, **kwargs) 21 | 22 | self.fields['transaction_type'].disabled = True 23 | self.fields['transaction_type'].widget = forms.HiddenInput() 24 | 25 | def save(self, commit=True): 26 | self.instance.account = self.account 27 | self.instance.balance_after_transaction = self.account.balance 28 | return super().save() 29 | 30 | 31 | class DepositForm(TransactionForm): 32 | 33 | def clean_amount(self): 34 | min_deposit_amount = settings.MINIMUM_DEPOSIT_AMOUNT 35 | amount = self.cleaned_data.get('amount') 36 | 37 | if amount < min_deposit_amount: 38 | raise forms.ValidationError( 39 | f'You need to deposit at least {min_deposit_amount} $' 40 | ) 41 | 42 | return amount 43 | 44 | 45 | class WithdrawForm(TransactionForm): 46 | 47 | def clean_amount(self): 48 | account = self.account 49 | min_withdraw_amount = settings.MINIMUM_WITHDRAWAL_AMOUNT 50 | max_withdraw_amount = ( 51 | account.account_type.maximum_withdrawal_amount 52 | ) 53 | balance = account.balance 54 | 55 | amount = self.cleaned_data.get('amount') 56 | 57 | if amount < min_withdraw_amount: 58 | raise forms.ValidationError( 59 | f'You can withdraw at least {min_withdraw_amount} $' 60 | ) 61 | 62 | if amount > max_withdraw_amount: 63 | raise forms.ValidationError( 64 | f'You can withdraw at most {max_withdraw_amount} $' 65 | ) 66 | 67 | if amount > balance: 68 | raise forms.ValidationError( 69 | f'You have {balance} $ in your account. ' 70 | 'You can not withdraw more than your account balance' 71 | ) 72 | 73 | return amount 74 | 75 | 76 | class TransactionDateRangeForm(forms.Form): 77 | daterange = forms.CharField(required=False) 78 | 79 | def clean_daterange(self): 80 | daterange = self.cleaned_data.get("daterange") 81 | print(daterange) 82 | 83 | try: 84 | daterange = daterange.split(' - ') 85 | print(daterange) 86 | if len(daterange) == 2: 87 | for date in daterange: 88 | datetime.datetime.strptime(date, '%Y-%m-%d') 89 | return daterange 90 | else: 91 | raise forms.ValidationError("Please select a date range.") 92 | except (ValueError, AttributeError): 93 | raise forms.ValidationError("Invalid date range") 94 | -------------------------------------------------------------------------------- /templates/transactions/transaction_report.html: -------------------------------------------------------------------------------- 1 | {% extends 'core/base.html' %} 2 | 3 | {% block head_title %}Transaction Report{% endblock %} 4 | 5 | {% block head_extra %} 6 | 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |

Transaction Report

14 |
15 |
16 |
17 | 18 | 23 |
24 | {% if form.daterange.errors %} 25 | {% for error in form.daterange.errors %} 26 |

{{ error }}

27 | {% endfor %} 28 | {% endif %} 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for transaction in object_list %} 41 | 42 | 43 | 44 | 45 | 46 | 47 | {% endfor %} 48 | 49 | 50 | 51 | 52 | 53 |
Transaction TypeDateAmountBalance After Transaction
{{ transaction.get_transaction_type_display }}{{ transaction.timestamp }}$ {{ transaction.amount }}$ {{ transaction.balance_after_transaction }}
Final Balance$ {{ account.balance }}
54 | {% endblock %} 55 | 56 | {% block footer_extra %} 57 | 75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from django.contrib.auth.models import AbstractUser 4 | from django.core.validators import ( 5 | MinValueValidator, 6 | MaxValueValidator, 7 | ) 8 | from django.db import models 9 | 10 | from .constants import GENDER_CHOICE 11 | from .managers import UserManager 12 | 13 | 14 | class User(AbstractUser): 15 | username = None 16 | email = models.EmailField(unique=True, null=False, blank=False) 17 | 18 | objects = UserManager() 19 | 20 | USERNAME_FIELD = 'email' 21 | REQUIRED_FIELDS = [] 22 | 23 | def __str__(self): 24 | return self.email 25 | 26 | @property 27 | def balance(self): 28 | if hasattr(self, 'account'): 29 | return self.account.balance 30 | return 0 31 | 32 | 33 | class BankAccountType(models.Model): 34 | name = models.CharField(max_length=128) 35 | maximum_withdrawal_amount = models.DecimalField( 36 | decimal_places=2, 37 | max_digits=12 38 | ) 39 | annual_interest_rate = models.DecimalField( 40 | validators=[MinValueValidator(0), MaxValueValidator(100)], 41 | decimal_places=2, 42 | max_digits=5, 43 | help_text='Interest rate from 0 - 100' 44 | ) 45 | interest_calculation_per_year = models.PositiveSmallIntegerField( 46 | validators=[MinValueValidator(1), MaxValueValidator(12)], 47 | help_text='The number of times interest will be calculated per year' 48 | ) 49 | 50 | def __str__(self): 51 | return self.name 52 | 53 | def calculate_interest(self, principal): 54 | """ 55 | Calculate interest for each account type. 56 | 57 | This uses a basic interest calculation formula 58 | """ 59 | p = principal 60 | r = self.annual_interest_rate 61 | n = Decimal(self.interest_calculation_per_year) 62 | 63 | # Basic Future Value formula to calculate interest 64 | interest = (p * (1 + ((r/100) / n))) - p 65 | 66 | return round(interest, 2) 67 | 68 | 69 | class UserBankAccount(models.Model): 70 | user = models.OneToOneField( 71 | User, 72 | related_name='account', 73 | on_delete=models.CASCADE, 74 | ) 75 | account_type = models.ForeignKey( 76 | BankAccountType, 77 | related_name='accounts', 78 | on_delete=models.CASCADE 79 | ) 80 | account_no = models.PositiveIntegerField(unique=True) 81 | gender = models.CharField(max_length=1, choices=GENDER_CHOICE) 82 | birth_date = models.DateField(null=True, blank=True) 83 | balance = models.DecimalField( 84 | default=0, 85 | max_digits=12, 86 | decimal_places=2 87 | ) 88 | interest_start_date = models.DateField( 89 | null=True, blank=True, 90 | help_text=( 91 | 'The month number that interest calculation will start from' 92 | ) 93 | ) 94 | initial_deposit_date = models.DateField(null=True, blank=True) 95 | 96 | def __str__(self): 97 | return str(self.account_no) 98 | 99 | def get_interest_calculation_months(self): 100 | """ 101 | List of month numbers for which the interest will be calculated 102 | 103 | returns [2, 4, 6, 8, 10, 12] for every 2 months interval 104 | """ 105 | interval = int( 106 | 12 / self.account_type.interest_calculation_per_year 107 | ) 108 | start = self.interest_start_date.month 109 | return [i for i in range(start, 13, interval)] 110 | 111 | 112 | class UserAddress(models.Model): 113 | user = models.OneToOneField( 114 | User, 115 | related_name='address', 116 | on_delete=models.CASCADE, 117 | ) 118 | street_address = models.CharField(max_length=512) 119 | city = models.CharField(max_length=256) 120 | postal_code = models.PositiveIntegerField() 121 | country = models.CharField(max_length=256) 122 | 123 | def __str__(self): 124 | return self.user.email 125 | -------------------------------------------------------------------------------- /banking_system/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for banking_system project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'po0172$69b@78ps4v^uhfxu6q--8ko7kpp7rbz420s_3w#sir%' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'django_celery_beat', 42 | 43 | 'accounts', 44 | 'core', 45 | 'transactions', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'banking_system.urls' 59 | AUTH_USER_MODEL = 'accounts.User' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [BASE_DIR / 'templates'], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'banking_system.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.sqlite3', 86 | 'NAME': BASE_DIR / 'db.sqlite3', 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 126 | 127 | STATIC_URL = '/static/' 128 | 129 | ACCOUNT_NUMBER_START_FROM = 1000000000 130 | MINIMUM_DEPOSIT_AMOUNT = 10 131 | MINIMUM_WITHDRAWAL_AMOUNT = 10 132 | 133 | # Login redirect 134 | LOGIN_REDIRECT_URL = 'home' 135 | 136 | # Celery Settings 137 | CELERY_BROKER_URL = 'redis://localhost:6379' 138 | CELERY_RESULT_BACKEND = 'redis://localhost:6379' 139 | CELERY_ACCEPT_CONTENT = ['application/json'] 140 | CELERY_TASK_SERIALIZER = 'json' 141 | CELERY_RESULT_SERIALIZER = 'json' 142 | CELERY_TIMEZONE = TIME_ZONE 143 | -------------------------------------------------------------------------------- /transactions/views.py: -------------------------------------------------------------------------------- 1 | from dateutil.relativedelta import relativedelta 2 | 3 | from django.contrib import messages 4 | from django.contrib.auth.mixins import LoginRequiredMixin 5 | from django.urls import reverse_lazy 6 | from django.utils import timezone 7 | from django.views.generic import CreateView, ListView 8 | 9 | from transactions.constants import DEPOSIT, WITHDRAWAL 10 | from transactions.forms import ( 11 | DepositForm, 12 | TransactionDateRangeForm, 13 | WithdrawForm, 14 | ) 15 | from transactions.models import Transaction 16 | 17 | 18 | class TransactionRepostView(LoginRequiredMixin, ListView): 19 | template_name = 'transactions/transaction_report.html' 20 | model = Transaction 21 | form_data = {} 22 | 23 | def get(self, request, *args, **kwargs): 24 | form = TransactionDateRangeForm(request.GET or None) 25 | if form.is_valid(): 26 | self.form_data = form.cleaned_data 27 | 28 | return super().get(request, *args, **kwargs) 29 | 30 | def get_queryset(self): 31 | queryset = super().get_queryset().filter( 32 | account=self.request.user.account 33 | ) 34 | 35 | daterange = self.form_data.get("daterange") 36 | 37 | if daterange: 38 | queryset = queryset.filter(timestamp__date__range=daterange) 39 | 40 | return queryset.distinct() 41 | 42 | def get_context_data(self, **kwargs): 43 | context = super().get_context_data(**kwargs) 44 | context.update({ 45 | 'account': self.request.user.account, 46 | 'form': TransactionDateRangeForm(self.request.GET or None) 47 | }) 48 | 49 | return context 50 | 51 | 52 | class TransactionCreateMixin(LoginRequiredMixin, CreateView): 53 | template_name = 'transactions/transaction_form.html' 54 | model = Transaction 55 | title = '' 56 | success_url = reverse_lazy('transactions:transaction_report') 57 | 58 | def get_form_kwargs(self): 59 | kwargs = super().get_form_kwargs() 60 | kwargs.update({ 61 | 'account': self.request.user.account 62 | }) 63 | return kwargs 64 | 65 | def get_context_data(self, **kwargs): 66 | context = super().get_context_data(**kwargs) 67 | context.update({ 68 | 'title': self.title 69 | }) 70 | 71 | return context 72 | 73 | 74 | class DepositMoneyView(TransactionCreateMixin): 75 | form_class = DepositForm 76 | title = 'Deposit Money to Your Account' 77 | 78 | def get_initial(self): 79 | initial = {'transaction_type': DEPOSIT} 80 | return initial 81 | 82 | def form_valid(self, form): 83 | amount = form.cleaned_data.get('amount') 84 | account = self.request.user.account 85 | 86 | if not account.initial_deposit_date: 87 | now = timezone.now() 88 | next_interest_month = int( 89 | 12 / account.account_type.interest_calculation_per_year 90 | ) 91 | account.initial_deposit_date = now 92 | account.interest_start_date = ( 93 | now + relativedelta( 94 | months=+next_interest_month 95 | ) 96 | ) 97 | 98 | account.balance += amount 99 | account.save( 100 | update_fields=[ 101 | 'initial_deposit_date', 102 | 'balance', 103 | 'interest_start_date' 104 | ] 105 | ) 106 | 107 | messages.success( 108 | self.request, 109 | f'{amount}$ was deposited to your account successfully' 110 | ) 111 | 112 | return super().form_valid(form) 113 | 114 | 115 | class WithdrawMoneyView(TransactionCreateMixin): 116 | form_class = WithdrawForm 117 | title = 'Withdraw Money from Your Account' 118 | 119 | def get_initial(self): 120 | initial = {'transaction_type': WITHDRAWAL} 121 | return initial 122 | 123 | def form_valid(self, form): 124 | amount = form.cleaned_data.get('amount') 125 | 126 | self.request.user.account.balance -= form.cleaned_data.get('amount') 127 | self.request.user.account.save(update_fields=['balance']) 128 | 129 | messages.success( 130 | self.request, 131 | f'Successfully withdrawn {amount}$ from your account' 132 | ) 133 | 134 | return super().form_valid(form) 135 | -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-09-01 16:27 2 | 3 | import accounts.managers 4 | from django.conf import settings 5 | import django.core.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django.utils.timezone 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ('auth', '0012_alter_user_first_name_max_length'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='User', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('password', models.CharField(max_length=128, verbose_name='password')), 25 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 26 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 27 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 28 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('email', models.EmailField(max_length=254, unique=True)), 33 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 34 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 35 | ], 36 | options={ 37 | 'verbose_name': 'user', 38 | 'verbose_name_plural': 'users', 39 | 'abstract': False, 40 | }, 41 | managers=[ 42 | ('objects', accounts.managers.UserManager()), 43 | ], 44 | ), 45 | migrations.CreateModel( 46 | name='BankAccountType', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('name', models.CharField(max_length=128)), 50 | ('maximum_withdrawal_amount', models.DecimalField(decimal_places=2, max_digits=12)), 51 | ('annual_interest_rate', models.DecimalField(decimal_places=2, help_text='Interest rate from 0 - 100', max_digits=5, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])), 52 | ('interest_calculation_per_year', models.PositiveSmallIntegerField(help_text='The number of times interest will be calculated per year', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(12)])), 53 | ], 54 | ), 55 | migrations.CreateModel( 56 | name='UserBankAccount', 57 | fields=[ 58 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 59 | ('account_no', models.PositiveIntegerField(unique=True)), 60 | ('gender', models.CharField(choices=[('M', 'Male'), ('F', 'Female')], max_length=1)), 61 | ('birth_date', models.DateField(blank=True, null=True)), 62 | ('balance', models.DecimalField(decimal_places=2, default=0, max_digits=12)), 63 | ('interest_start_date', models.DateField(blank=True, help_text='The month number that interest calculation will start from', null=True)), 64 | ('initial_deposit_date', models.DateField(blank=True, null=True)), 65 | ('account_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='accounts.bankaccounttype')), 66 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='account', to=settings.AUTH_USER_MODEL)), 67 | ], 68 | ), 69 | migrations.CreateModel( 70 | name='UserAddress', 71 | fields=[ 72 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 73 | ('street_address', models.CharField(max_length=512)), 74 | ('city', models.CharField(max_length=256)), 75 | ('postal_code', models.PositiveIntegerField()), 76 | ('country', models.CharField(max_length=256)), 77 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='address', to=settings.AUTH_USER_MODEL)), 78 | ], 79 | ), 80 | ] 81 | -------------------------------------------------------------------------------- /templates/accounts/user_registration.html: -------------------------------------------------------------------------------- 1 | {% extends 'core/base.html' %} 2 | 3 | {% block head_title %}Banking System{% endblock %} 4 | 5 | {% block content %} 6 | {% if registration_form.non_field_errors %} 7 | {% for error in registration_form.non_field_errors %} 8 | 12 | {% endfor %} 13 | {% endif %} 14 | {% if address_form.non_field_errors %} 15 | {% for error in address_form.non_field_errors %} 16 | 20 | {% endfor %} 21 | {% endif %} 22 | 23 |

Register

24 |
25 |
26 |
27 | {% csrf_token %} 28 | {% for hidden_field in registration_form.hidden_fields %} 29 | {{ hidden_field.errors }} 30 | {{ hidden_field }} 31 | {% endfor %} 32 |
33 |
34 | 37 | {{ registration_form.first_name }} 38 | {% if registration_form.first_name.errors %} 39 | {% for error in registration_form.first_name.errors %} 40 |

{{ error }}

41 | {% endfor %} 42 | {% endif %} 43 |
44 |
45 | 48 | {{ registration_form.last_name }} 49 | {% if registration_form.last_name.errors %} 50 | {% for error in registration_form.last_name.errors %} 51 |

{{ error }}

52 | {% endfor %} 53 | {% endif %} 54 |
55 |
56 |
57 |
58 | 61 | {{ registration_form.email }} 62 | {% if registration_form.email.errors %} 63 | {% for error in registration_form.email.errors %} 64 |

{{ error }}

65 | {% endfor %} 66 | {% endif %} 67 |
68 |
69 | 72 | {{ registration_form.account_type }} 73 | {% if registration_form.account_type.errors %} 74 | {% for error in registration_form.account_type.errors %} 75 |

{{ error }}

76 | {% endfor %} 77 | {% endif %} 78 |
79 |
80 |
81 |
82 | 85 | {{ registration_form.gender }} 86 | {% if registration_form.gender.errors %} 87 | {% for error in registration_form.gender.errors %} 88 |

{{ error }}

89 | {% endfor %} 90 | {% endif %} 91 |
92 |
93 | 96 | {{ registration_form.birth_date }} 97 | {% if registration_form.birth_date.errors %} 98 | {% for error in registration_form.birth_date.errors %} 99 |

{{ error }}

100 | {% endfor %} 101 | {% endif %} 102 |
103 |
104 |
105 |
106 | 109 | {{ registration_form.password1 }} 110 | {% if registration_form.password1.errors %} 111 | {% for error in registration_form.password1.errors %} 112 |

{{ error }}

113 | {% endfor %} 114 | {% endif %} 115 |
116 |
117 | 120 | {{ registration_form.password2 }} 121 | {% if registration_form.password2.errors %} 122 | {% for error in registration_form.password2.errors %} 123 |

{{ error }}

124 | {% endfor %} 125 | {% endif %} 126 |
127 |
128 | 129 | {% for hidden_field in address_form.hidden_fields %} {{ hidden_field.errors }} {{ hidden_field }} {% endfor %} 130 |
131 |
132 | 135 | {{ address_form.street_address }} 136 | {% if address_form.street_address.errors %} 137 | {% for error in address_form.street_address.errors %} 138 |

{{ error }}

139 | {% endfor %} 140 | {% endif %} 141 |
142 |
143 | 146 | {{ address_form.city }} 147 | {% if address_form.city.errors %} 148 | {% for error in address_form.city.errors %} 149 |

{{ error }}

150 | {% endfor %} 151 | {% endif %} 152 |
153 |
154 |
155 |
156 | 159 | {{ address_form.postal_code }} 160 | {% if address_form.postal_code.errors %} 161 | {% for error in address_form.postal_code.errors %} 162 |

{{ error }}

163 | {% endfor %} 164 | {% endif %} 165 |
166 |
167 | 170 | {{ address_form.country }} 171 | {% if address_form.country.errors %} 172 | {% for error in address_form.country.errors %} 173 |

{{ error }}

174 | {% endfor %} 175 | {% endif %} 176 |
177 |
178 |
179 | 182 |
183 |
184 |
185 | {% endblock %} 186 | --------------------------------------------------------------------------------