├── src ├── api │ ├── __init__.py │ └── urls │ │ ├── __init__.py │ │ └── urls_v1.py ├── cores │ ├── __init__.py │ ├── models.py │ └── permissions.py ├── customers │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── urls.py │ ├── admin.py │ ├── models.py │ ├── views.py │ ├── serializers.py │ └── tests.py ├── administrations │ ├── __init__.py │ ├── services.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_bankstatement_description.py │ │ └── 0001_initial.py │ ├── admin.py │ ├── apps.py │ ├── urls.py │ ├── models.py │ ├── views.py │ ├── serializers.py │ └── tests.py ├── simplebanking │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py └── manage.py ├── .gitignore ├── requirements.txt ├── Dockerfile ├── conf └── nginx.conf ├── .github └── workflows │ ├── django.yml │ └── codeql-analysis.yml ├── Makefile ├── docker-compose.yml ├── README.md └── Simple BANK.postman_collection.json /src/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cores/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/customers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/administrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/administrations/services.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/simplebanking/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/customers/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/administrations/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/* 3 | .vscode/* 4 | venv/* 5 | src/static/* 6 | media/* 7 | pgdata/* 8 | -------------------------------------------------------------------------------- /src/administrations/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /src/customers/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CustomersConfig(AppConfig): 5 | name = 'customers' 6 | -------------------------------------------------------------------------------- /src/administrations/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AdministrationsConfig(AppConfig): 5 | name = 'administrations' 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coreapi==2.3.3 2 | coreschema==0.0.4 3 | Django==3.1.14 4 | djangorestframework==3.11.2 5 | djangorestframework-simplejwt==4.4.0 6 | gunicorn==19.9.0 7 | psycopg2-binary==2.8.5 8 | PyJWT==2.4.0 9 | -------------------------------------------------------------------------------- /src/customers/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework import routers 2 | from . import views 3 | 4 | app_name = 'customers' 5 | 6 | router = routers.SimpleRouter() 7 | router.register('', views.CustomerViewSet) 8 | 9 | urlpatterns = router.urls -------------------------------------------------------------------------------- /src/api/urls/urls_v1.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | app_name = 'router_v1' 4 | urlpatterns = [ 5 | path('customers/', include('customers.urls', namespace='customers')), 6 | path('accounts/', include('administrations.urls', namespace='accounts')), 7 | ] -------------------------------------------------------------------------------- /src/cores/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class CommonInfo(models.Model): 5 | created = models.DateTimeField(auto_now_add=True) 6 | modified = models.DateTimeField(auto_now=True) 7 | deleted_at = models.DateTimeField(null=True, blank=True) 8 | is_deleted = models.BooleanField(default=False) 9 | 10 | class Meta: 11 | abstract = True 12 | -------------------------------------------------------------------------------- /src/customers/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Customer 4 | 5 | 6 | class CustomerAdmin(admin.ModelAdmin): 7 | list_display = [ 8 | 'user', 'sex', 'identity_number', 9 | ] 10 | search_fields = [ 11 | 'guid', 'user__username', 'user__email', 'user__first_name', 12 | 'user__last_name', 13 | ] 14 | list_filter = ['sex'] 15 | 16 | 17 | admin.site.register(Customer, CustomerAdmin) 18 | -------------------------------------------------------------------------------- /src/administrations/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework import routers 2 | 3 | from . import views 4 | 5 | app_name = 'administrations' 6 | 7 | router = routers.SimpleRouter() 8 | router.register('', views.BankInformationViewSet) 9 | router.register('deposit', views.DepositViewSet, basename='deposit') 10 | router.register('transfer', views.TransferViewSet, basename='transfer') 11 | router.register('withdraw', views.WithdrawViewSet, basename='withdraw') 12 | 13 | urlpatterns = router.urls 14 | -------------------------------------------------------------------------------- /src/simplebanking/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for simplebanking 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/2.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', 'simplebanking.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/cores/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import IsAuthenticated 2 | 3 | 4 | class IsCustomer(IsAuthenticated): 5 | 6 | def has_permission(self, request, view): 7 | authenticated = super(IsCustomer, self).has_permission(request, view) 8 | return authenticated and hasattr(request.user, 'customer') 9 | 10 | 11 | class IsBankOwner(IsCustomer): 12 | 13 | def has_object_permission(self, request, view, obj): 14 | return obj.holder == request.user.customer 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | ENV PYTHONUNBUFFERED 1 3 | 4 | RUN mkdir /sourcecode 5 | WORKDIR /sourcecode 6 | COPY src /sourcecode/ 7 | COPY requirements.txt /sourcecode/ 8 | 9 | RUN apk add\ 10 | abuild\ 11 | binutils\ 12 | binutils-doc\ 13 | build-base\ 14 | gcc\ 15 | gcc-doc\ 16 | postgresql-dev\ 17 | && rm -rf /var/cache/apk/* 18 | RUN pip install -U pip && pip install --no-cache-dir -r requirements.txt 19 | 20 | CMD gunicorn simplebanking.wsgi -b 0.0.0.0:5000 --log-level=debug --log-file=- -------------------------------------------------------------------------------- /src/administrations/migrations/0002_bankstatement_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-12-03 16:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('administrations', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='bankstatement', 15 | name='description', 16 | field=models.TextField(default=''), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'simplebanking.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /conf/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | upstream backend_api { 3 | server bank_api:5000; 4 | } 5 | 6 | server { 7 | listen 80; 8 | charset utf-8; 9 | access_log off; 10 | 11 | location / { 12 | proxy_pass http://backend_api/; 13 | proxy_set_header Host $host:$server_port; 14 | proxy_set_header X-Forwarded-Host $server_name; 15 | proxy_set_header X-Real-IP $remote_addr; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | } 18 | 19 | location /static { 20 | access_log off; 21 | expires 30d; 22 | 23 | alias /static; 24 | } 25 | } -------------------------------------------------------------------------------- /src/customers/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.contrib.auth.models import User 4 | from django.db import models 5 | 6 | from cores.models import CommonInfo 7 | 8 | 9 | class Customer(CommonInfo): 10 | MALE = 'male' 11 | FEMALE = 'female' 12 | SEX_CHOICES = ( 13 | (MALE, 'Male'), 14 | (FEMALE, 'Female'), 15 | ) 16 | 17 | guid = models.UUIDField(unique=True, editable=False, default=uuid.uuid4) 18 | identity_number = models.CharField(max_length=60, unique=True) 19 | address = models.TextField() 20 | user = models.OneToOneField(User, on_delete=models.CASCADE) 21 | sex = models.CharField(choices=SEX_CHOICES, max_length=6) 22 | 23 | def __str__(self): 24 | return self.user.username 25 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.8] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v1 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | - name: Run Tests 29 | run: | 30 | cd src 31 | python manage.py test 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo '' 3 | @echo 'Usage: make [TARGET]' 4 | @echo 'Targets:' 5 | @echo ' build build docker images' 6 | @echo ' start Start banking system service' 7 | @echo ' stop Stop banking system service' 8 | @echo ' test test banking system service' 9 | @echo ' help this text' 10 | 11 | build: 12 | docker-compose build 13 | 14 | start: 15 | @echo "Starting banking system service..." 16 | docker-compose up -d 17 | 18 | @echo "Creating the bank database and performing migrations..." 19 | docker-compose exec bank_api python manage.py migrate 20 | docker-compose exec bank_api python manage.py collectstatic 21 | @echo "Done" 22 | 23 | stop: 24 | docker-compose down 25 | 26 | test: 27 | docker-compose exec bank_api python manage.py test --keepdb -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | reverse_proxy: 5 | restart: always 6 | image: nginx:1.19.0-alpine 7 | volumes: 8 | - ./conf:/etc/nginx/conf.d 9 | - ./src/static:/static 10 | ports: 11 | - "80:80" 12 | networks: 13 | - front-net 14 | 15 | bank_api: 16 | build: . 17 | expose: 18 | - "5000" 19 | depends_on: 20 | - bank_db 21 | volumes: 22 | - ./src:/sourcecode 23 | networks: 24 | - back-net 25 | - front-net 26 | 27 | bank_db: 28 | image: postgres:9.6-alpine 29 | restart: always 30 | volumes: 31 | - postgres:/var/lib/postgresql/data 32 | ports: 33 | - "5432:5432" 34 | networks: 35 | - back-net 36 | environment: 37 | - POSTGRES_USER=postgres 38 | - POSTGRES_PASSWORD=testing 39 | - POSTGRES_DB=db_banking 40 | 41 | networks: 42 | back-net: 43 | front-net: 44 | 45 | volumes: 46 | conf: 47 | static: 48 | postgres: -------------------------------------------------------------------------------- /src/customers/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import mixins, viewsets 2 | from rest_framework.permissions import AllowAny 3 | from rest_framework.response import Response 4 | 5 | from cores.permissions import IsCustomer 6 | from .models import Customer 7 | from .serializers import CustomerSerializer 8 | 9 | 10 | class CustomerViewSet(mixins.CreateModelMixin, 11 | mixins.ListModelMixin, 12 | viewsets.GenericViewSet): 13 | queryset = Customer.objects.filter(is_deleted=False).order_by('-created') 14 | serializer_class = CustomerSerializer 15 | permission_classes = [IsCustomer] 16 | 17 | def get_permissions(self): 18 | if self.action == 'create': 19 | return [AllowAny()] 20 | return super(CustomerViewSet, self).get_permissions() 21 | 22 | def list(self, request, *args, **kwargs): 23 | customer = request.user.customer 24 | serializer = self.get_serializer(instance=customer) 25 | 26 | return Response(serializer.data) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple banking API Demo 2 | 3 | Demo of simple banking system API. 4 | Built on top of python stack using Django, Gunicorn, PostgreSQL and Nginx 5 | 6 | 7 | ### Setup and installation (Docker): 8 | 9 | - Build docker image 10 | ```sh 11 | $ make build 12 | ``` 13 | 14 | - Start banking system service 15 | ```sh 16 | $ make start 17 | ``` 18 | 19 | - Stop banking system service 20 | ```sh 21 | $ make stop 22 | ``` 23 | 24 | - Run tests for this project 25 | ```sh 26 | $ make test 27 | ``` 28 | 29 | - make command usage details 30 | ```sh 31 | $ make help 32 | ``` 33 | 34 | ### Setup and installation (Manual): 35 | 36 | Pre-requisite: 37 | - Python 3.6 38 | - PostgreSQL 39 | 40 | You can go through following below to start the project. 41 | 42 | ``` 43 | $ createdb --username=postgres simplebankdb 44 | $ pip install -r requirements.txt 45 | $ ./manage.py migrate 46 | $ ./manage.py runserver 47 | ``` 48 | 49 | And you can run the project test using following command. 50 | 51 | ``` 52 | $ ./manage.py test --keepdb 53 | ``` 54 | 55 | ### API Docs. 56 | 57 | Endpoints for this project are documented in `/docs/` 58 | 59 | But you can also import [postman collections](Simple BANK.postman_collection.json) within this project for more convenience. 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/customers/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-29 16:42 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Customer', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', models.DateTimeField(auto_now_add=True)), 23 | ('modified', models.DateTimeField(auto_now=True)), 24 | ('deleted_at', models.DateTimeField(blank=True, null=True)), 25 | ('is_deleted', models.BooleanField(default=False)), 26 | ('guid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 27 | ('identity_number', models.CharField(max_length=60, unique=True)), 28 | ('address', models.TextField()), 29 | ('sex', models.CharField(choices=[('male', 'Male'), ('female', 'Female')], max_length=6)), 30 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 31 | ], 32 | options={ 33 | 'abstract': False, 34 | }, 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /src/simplebanking/urls.py: -------------------------------------------------------------------------------- 1 | """simplebanking URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.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 datetime import datetime 17 | 18 | from django.contrib import admin 19 | from django.http import JsonResponse, HttpRequest 20 | from django.urls import path, include 21 | from rest_framework.documentation import include_docs_urls 22 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView 23 | 24 | 25 | def home(request: HttpRequest) -> JsonResponse: 26 | return JsonResponse({ 27 | 'host': request.get_host(), 28 | 'status': 'ok', 29 | 'dateTime': datetime.utcnow(), 30 | }) 31 | 32 | 33 | urlpatterns = [ 34 | path('', home), 35 | path('api/v1/', include('api.urls.urls_v1', namespace='v1')), 36 | path('admin/', admin.site.urls), 37 | path('docs/', include_docs_urls(title='Simple Bank API')), 38 | path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 39 | path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 40 | ] 41 | -------------------------------------------------------------------------------- /src/administrations/models.py: -------------------------------------------------------------------------------- 1 | import random 2 | import uuid 3 | 4 | from django.db import models 5 | from django.db.models import Sum 6 | 7 | from cores.models import CommonInfo 8 | 9 | 10 | class BankInformation(CommonInfo): 11 | guid = models.UUIDField(unique=True, editable=False, default=uuid.uuid4) 12 | account_number = models.CharField(max_length=15, unique=True) 13 | holder = models.OneToOneField('customers.Customer', on_delete=models.CASCADE) 14 | is_active = models.BooleanField(default=False) 15 | 16 | @property 17 | def total_balance(self): 18 | aggregate_credit = BankStatement.objects.filter( 19 | bank_info__pk=self.pk, 20 | is_debit=False, 21 | ).aggregate(total=Sum('amount')) 22 | 23 | aggregate_debit = BankStatement.objects.filter( 24 | bank_info__pk=self.pk, 25 | is_debit=True, 26 | ).aggregate(total=Sum('amount')) 27 | 28 | credit = aggregate_credit.get('total') 29 | credit = credit if credit is not None else 0 30 | 31 | debit = aggregate_debit.get('total') 32 | debit = debit if debit is not None else 0 33 | 34 | return credit - debit 35 | 36 | @classmethod 37 | def generate_account_number(cls): 38 | return ''.join(random.choice('0123456789ABCDEFGH') for _ in range(13)) 39 | 40 | def __str__(self): 41 | return self.account_number 42 | 43 | 44 | class BankStatement(CommonInfo): 45 | bank_info = models.ForeignKey( # Bank information for the receiver. 46 | BankInformation, 47 | related_name='mutations', 48 | on_delete=models.CASCADE, 49 | ) 50 | sender = models.ForeignKey( 51 | 'customers.Customer', 52 | related_name='sender', 53 | on_delete=models.CASCADE, 54 | ) 55 | receiver = models.ForeignKey( 56 | 'customers.Customer', 57 | related_name='receiver', 58 | on_delete=models.CASCADE, 59 | ) 60 | amount = models.DecimalField(max_digits=12, decimal_places=2) 61 | is_debit = models.BooleanField(default=False) 62 | description = models.TextField() 63 | 64 | def __str__(self): 65 | return (f'Owner: {self.bank_info.holder.user.get_full_name()} ' 66 | f'{"Debit: " if self.is_debit else "Credit: "} {self.amount}') 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/customers/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import transaction 3 | from rest_framework import serializers 4 | 5 | from administrations.models import BankInformation 6 | from customers.models import Customer 7 | 8 | 9 | class SimpleUserSerializer(serializers.ModelSerializer): 10 | 11 | class Meta: 12 | model = User 13 | fields = [ 14 | 'id', 'email', 'username', 'first_name', 'last_name', 15 | ] 16 | 17 | 18 | class CustomerSignUpSerializer(serializers.Serializer): 19 | first_name = serializers.CharField(write_only=True) 20 | last_name = serializers.CharField(write_only=True) 21 | email = serializers.EmailField(write_only=True) 22 | password = serializers.CharField(write_only=True) 23 | 24 | def validate(self, attrs): 25 | email = attrs.get('email') 26 | if User.objects.filter(username=email).exists(): 27 | raise serializers.ValidationError({ 28 | 'username': 'This username is already taken.' 29 | }) 30 | return attrs 31 | 32 | 33 | class CustomerSerializer(CustomerSignUpSerializer, 34 | serializers.ModelSerializer): 35 | user = SimpleUserSerializer(read_only=True) 36 | 37 | @transaction.atomic 38 | def create(self, validated_data): 39 | email = validated_data.get('email') 40 | 41 | user = User.objects.create_user( 42 | username=email, 43 | email=email, 44 | password=validated_data.get('password'), 45 | first_name=validated_data.get('first_name'), 46 | last_name=validated_data.get('last_name'), 47 | ) 48 | 49 | customer = Customer.objects.create( 50 | identity_number=validated_data.get('identity_number'), 51 | address=validated_data.get('address'), 52 | sex=validated_data.get('sex'), 53 | user=user, 54 | ) 55 | 56 | BankInformation.objects.create( 57 | account_number=BankInformation.generate_account_number(), 58 | holder=customer, 59 | ) 60 | 61 | return customer 62 | 63 | class Meta: 64 | model = Customer 65 | fields = [ 66 | 'id', 'guid', 'address', 'sex', 'identity_number', 'user', 67 | 'first_name', 'last_name', 'email', 'password', 68 | ] 69 | read_only_fields = ['id', 'guid'] 70 | -------------------------------------------------------------------------------- /src/administrations/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-12-02 05:04 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('customers', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='BankInformation', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('created', models.DateTimeField(auto_now_add=True)), 22 | ('modified', models.DateTimeField(auto_now=True)), 23 | ('deleted_at', models.DateTimeField(blank=True, null=True)), 24 | ('is_deleted', models.BooleanField(default=False)), 25 | ('guid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 26 | ('account_number', models.CharField(max_length=15, unique=True)), 27 | ('is_active', models.BooleanField(default=False)), 28 | ('holder', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='customers.Customer')), 29 | ], 30 | options={ 31 | 'abstract': False, 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='BankStatement', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('created', models.DateTimeField(auto_now_add=True)), 39 | ('modified', models.DateTimeField(auto_now=True)), 40 | ('deleted_at', models.DateTimeField(blank=True, null=True)), 41 | ('is_deleted', models.BooleanField(default=False)), 42 | ('amount', models.DecimalField(decimal_places=2, max_digits=12)), 43 | ('is_debit', models.BooleanField(default=False)), 44 | ('bank_info', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mutations', to='administrations.BankInformation')), 45 | ('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='receiver', to='customers.Customer')), 46 | ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sender', to='customers.Customer')), 47 | ], 48 | options={ 49 | 'abstract': False, 50 | }, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '23 20 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /src/customers/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import status 3 | from rest_framework.reverse import reverse 4 | from rest_framework.test import APITestCase, APIRequestFactory, force_authenticate 5 | 6 | from customers.views import CustomerViewSet 7 | from .models import Customer 8 | 9 | 10 | class CustomerAPITest(APITestCase): 11 | 12 | def setUp(self): 13 | self.factory = APIRequestFactory() 14 | 15 | def test_register_customer(self): 16 | payload = { 17 | 'first_name': 'adiyat', 18 | 'last_name': 'mubarak', 19 | 'address': 'JL Dorowati barat 33', 20 | 'sex': Customer.MALE, 21 | 'identity_number': '0123456789', 22 | 'email': 'adiyatmubarak@gmail.com', 23 | 'password': 'testing123', 24 | } 25 | 26 | url = reverse('v1:customers:customer-list') 27 | request = self.factory.post(path=url, data=payload) 28 | view = CustomerViewSet.as_view({'post': 'create'}) 29 | response = view(request) 30 | customer = Customer.objects.get(user__username='adiyatmubarak@gmail.com') 31 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 32 | self.assertFalse(customer.bankinformation.is_active) 33 | 34 | def test_register_customer_with_email_taken(self): 35 | user = User.objects.create_user( 36 | username='tester@simplebank.com', 37 | email='tester@simplebank.com', 38 | password='testing123', 39 | first_name='qa', 40 | last_name='engineer', 41 | ) 42 | Customer.objects.create( 43 | identity_number='1234', 44 | address='Jauh alamat', 45 | sex=Customer.MALE, 46 | user=user, 47 | ) 48 | 49 | payload = { 50 | 'first_name': 'another', 51 | 'last_name': 'adit', 52 | 'sex': Customer.MALE, 53 | 'identity_number': '0123456789', 54 | 'email': 'tester@simplebank.com', 55 | 'password': 'testing123', 56 | 'address': 'alamat jauh sekali', 57 | } 58 | 59 | url = reverse('v1:customers:customer-list') 60 | request = self.factory.post(path=url, data=payload) 61 | view = CustomerViewSet.as_view({'post': 'create'}) 62 | response = view(request) 63 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 64 | self.assertEqual(response.data, { 65 | 'username': ['This username is already taken.'], 66 | }) 67 | 68 | def test_retrieve_customer_information(self): 69 | self.test_register_customer() 70 | customer = Customer.objects.get(user__username='adiyatmubarak@gmail.com') 71 | 72 | url = reverse('v1:customers:customer-list') 73 | request = self.factory.get(path=url) 74 | force_authenticate(request, customer.user) 75 | view = CustomerViewSet.as_view({'get': 'list'}) 76 | response = view(request) 77 | self.assertEqual(response.status_code, status.HTTP_200_OK) -------------------------------------------------------------------------------- /src/administrations/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins, status 2 | from rest_framework.decorators import action 3 | from rest_framework.response import Response 4 | 5 | from cores.permissions import IsBankOwner, IsCustomer 6 | from .models import BankInformation, BankStatement 7 | from .serializers import (AccountSerializer, DepositTransactionSerializer, 8 | TransferTransactionSerializer, WithdrawSerializer, 9 | MutationSerializer) 10 | 11 | 12 | class BankInformationViewSet(mixins.ListModelMixin, 13 | viewsets.GenericViewSet): 14 | serializer_class = AccountSerializer 15 | permission_classes = [IsCustomer] 16 | queryset = BankInformation.objects.filter(is_deleted=False) 17 | lookup_field = 'guid' 18 | 19 | def get_serializer_class(self): 20 | if self.action == 'mutations': 21 | return MutationSerializer 22 | return super(BankInformationViewSet, self).get_serializer_class() 23 | 24 | def get_queryset(self): 25 | queryet = super(BankInformationViewSet, self).get_queryset() 26 | return queryet.filter(holder=self.request.user.customer) 27 | 28 | def list(self, request, *args, **kwargs): 29 | customer = request.user.customer 30 | bank_info = customer.bankinformation 31 | data = self.get_serializer(instance=bank_info).data 32 | 33 | return Response(data) 34 | 35 | @action(methods=['put'], detail=True, permission_classes=[IsBankOwner]) 36 | def activate(self, request, **kwargs): 37 | bank_info = self.get_object() 38 | bank_info.is_active = True 39 | bank_info.save() 40 | 41 | return Response(status=status.HTTP_200_OK) 42 | 43 | @action(methods=['put'], detail=True, permission_classes=[IsBankOwner]) 44 | def deactivate(self, request, **kwargs): 45 | bank_info = self.get_object() 46 | bank_info.is_active = False 47 | bank_info.save() 48 | 49 | return Response(status=status.HTTP_200_OK) 50 | 51 | @action(methods=['get'], detail=True, permission_classes=[IsBankOwner]) 52 | def mutations(self, request, **kwargs): 53 | bank_info = self.get_object() 54 | mutations = bank_info.mutations.filter(is_deleted=False).order_by('-created') 55 | mutations = self.paginate_queryset(mutations) 56 | serializer = self.get_serializer(instance=mutations, many=True) 57 | 58 | return self.get_paginated_response(serializer.data) 59 | 60 | 61 | class DepositViewSet(mixins.CreateModelMixin, 62 | viewsets.GenericViewSet): 63 | serializer_class = DepositTransactionSerializer 64 | permission_classes = [IsCustomer] 65 | queryset = BankStatement.objects.filter(is_deleted=False) 66 | 67 | def create(self, request, *args, **kwargs): 68 | data = request.data.copy() 69 | data['sender'] = request.user.customer.pk 70 | serializer = self.get_serializer(data=data, context={'request': request}) 71 | if serializer.is_valid(): 72 | response = serializer.save() 73 | return Response(response, status.HTTP_201_CREATED) 74 | return Response(serializer.errors, status.HTTP_400_BAD_REQUEST) 75 | 76 | 77 | class TransferViewSet(mixins.CreateModelMixin, 78 | viewsets.GenericViewSet): 79 | serializer_class = TransferTransactionSerializer 80 | permission_classes = [IsCustomer] 81 | queryset = BankStatement.objects.filter(is_deleted=False) 82 | 83 | def create(self, request, *args, **kwargs): 84 | data = request.data.copy() 85 | data['sender'] = request.user.customer.pk 86 | serializer = self.get_serializer(data=data, context={'request': request}) 87 | if serializer.is_valid(): 88 | response = serializer.save() 89 | return Response(response, status.HTTP_201_CREATED) 90 | return Response(serializer.errors, status.HTTP_400_BAD_REQUEST) 91 | 92 | 93 | class WithdrawViewSet(mixins.CreateModelMixin, 94 | viewsets.GenericViewSet): 95 | serializer_class = WithdrawSerializer 96 | permission_classes = [IsCustomer] 97 | queryset = BankStatement.objects.filter(is_deleted=False) 98 | 99 | def create(self, request, *args, **kwargs): 100 | data = request.data.copy() 101 | data['sender'] = request.user.customer.pk 102 | serializer = self.get_serializer(data=data, context={'request': request}) 103 | if serializer.is_valid(): 104 | response = serializer.save() 105 | return Response(response, status.HTTP_201_CREATED) 106 | return Response(serializer.errors, status.HTTP_400_BAD_REQUEST) 107 | -------------------------------------------------------------------------------- /src/simplebanking/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for simplebanking project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | import sys 17 | 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = 'ul94!94-f*6_ky7cmx#^0z7v@@)8iav(*auza#@$v5ho$rl=-b' 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = ['*'] 31 | 32 | 33 | # Application definition 34 | DJANGO_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | ] 42 | 43 | THIRD_PARTY_APPS = [ 44 | 'rest_framework', 45 | ] 46 | 47 | LOCAL_APPS = [ 48 | 'cores', 49 | 'customers', 50 | 'administrations', 51 | ] 52 | 53 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 54 | 55 | MIDDLEWARE = [ 56 | 'django.middleware.security.SecurityMiddleware', 57 | 'django.contrib.sessions.middleware.SessionMiddleware', 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 63 | ] 64 | 65 | ROOT_URLCONF = 'simplebanking.urls' 66 | 67 | TEMPLATES = [ 68 | { 69 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 70 | 'DIRS': [], 71 | 'APP_DIRS': True, 72 | 'OPTIONS': { 73 | 'context_processors': [ 74 | 'django.template.context_processors.debug', 75 | 'django.template.context_processors.request', 76 | 'django.contrib.auth.context_processors.auth', 77 | 'django.contrib.messages.context_processors.messages', 78 | ], 79 | }, 80 | }, 81 | ] 82 | 83 | WSGI_APPLICATION = 'simplebanking.wsgi.application' 84 | 85 | 86 | # Database 87 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 88 | 89 | DATABASES = { 90 | 'default': { 91 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 92 | 'NAME': 'db_banking', 93 | 'USER': 'postgres', 94 | 'PASSWORD': 'testing', 95 | 'HOST': 'bank_db', 96 | 'PORT': '5432', 97 | 'CONN_MAX_AGE ': 60 98 | } 99 | } 100 | 101 | if 'test' in sys.argv: 102 | PASSWORD_HASHERS = [ 103 | 'django.contrib.auth.hashers.MD5PasswordHasher', 104 | ] 105 | 106 | DATABASES = { 107 | 'default': { 108 | 'ENGINE': 'django.db.backends.sqlite3', 109 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 110 | } 111 | } 112 | 113 | 114 | # Password validation 115 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 116 | 117 | AUTH_PASSWORD_VALIDATORS = [ 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 120 | }, 121 | { 122 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 123 | }, 124 | { 125 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 126 | }, 127 | { 128 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 129 | }, 130 | ] 131 | 132 | 133 | # Internationalization 134 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 135 | 136 | LANGUAGE_CODE = 'en-us' 137 | 138 | TIME_ZONE = 'UTC' 139 | 140 | USE_I18N = True 141 | 142 | USE_L10N = True 143 | 144 | USE_TZ = True 145 | 146 | 147 | # Static files (CSS, JavaScript, Images) 148 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 149 | 150 | STATIC_URL = '/static/' 151 | STATIC_ROOT = os.path.join(BASE_DIR, "static/") 152 | 153 | 154 | # --------------------------- DRF config --------------------------------------- 155 | REST_FRAMEWORK = { 156 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 157 | 'rest_framework.authentication.BasicAuthentication', 158 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 159 | ], 160 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 161 | 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 162 | 'PAGE_SIZE': 100 163 | } 164 | # ------------------------------------------------------------------------------ 165 | -------------------------------------------------------------------------------- /src/administrations/serializers.py: -------------------------------------------------------------------------------- 1 | from django.db import transaction 2 | from rest_framework import serializers 3 | 4 | from customers.models import Customer 5 | from .models import BankInformation, BankStatement 6 | 7 | 8 | class AccountSerializer(serializers.ModelSerializer): 9 | holder = serializers.CharField(source='holder.user.get_full_name') 10 | balance = serializers.DecimalField( 11 | source='total_balance', 12 | decimal_places=2, 13 | max_digits=12, 14 | ) 15 | 16 | class Meta: 17 | model = BankInformation 18 | fields = [ 19 | 'id', 'guid', 'account_number', 'holder', 'is_active', 'balance' 20 | ] 21 | 22 | 23 | class TransactionSerializer(serializers.ModelSerializer): 24 | bank_info = serializers.CharField(source='bank_info.account_number') 25 | sender = serializers.CharField(source='sender.user.get_full_name') 26 | receiver = serializers.CharField(source='receiver.user.get_full_name') 27 | 28 | class Meta: 29 | model = BankStatement 30 | fields = [ 31 | 'id', 'bank_info', 'sender', 'receiver', 'amount', 'is_debit', 32 | ] 33 | 34 | 35 | class DepositTransactionSerializer(serializers.Serializer): 36 | sender = serializers.PrimaryKeyRelatedField( 37 | queryset=Customer.objects.filter(is_deleted=False), 38 | ) 39 | amount = serializers.DecimalField(max_digits=12, decimal_places=2) 40 | 41 | def validate(self, attrs): 42 | sender = attrs.get('sender') 43 | if not sender.bankinformation.is_active: 44 | raise serializers.ValidationError({ 45 | 'sender': 'Bank account is blocked or inactive.' 46 | }) 47 | return attrs 48 | 49 | def create(self, validated_data): 50 | sender = validated_data.get('sender') 51 | deposit_amount = validated_data.get('amount') 52 | 53 | deposit = BankStatement() 54 | deposit.bank_info = sender.bankinformation 55 | deposit.sender = sender 56 | deposit.receiver = sender 57 | deposit.amount = deposit_amount 58 | deposit.is_debit = False 59 | deposit.description = 'Amount deposit' 60 | deposit.save() 61 | 62 | serializer = TransactionSerializer(instance=deposit) 63 | return serializer.data 64 | 65 | 66 | class WithdrawSerializer(DepositTransactionSerializer): 67 | 68 | @transaction.atomic 69 | def create(self, validated_data): 70 | sender = validated_data.get('sender') 71 | deposit_amount = validated_data.get('amount') 72 | 73 | sender_bank = BankInformation.objects.select_for_update().get( 74 | pk=sender.bankinformation.pk, 75 | ) 76 | 77 | if sender_bank.total_balance < deposit_amount: 78 | raise serializers.ValidationError({ 79 | 'amount': 'Insufficient funds.' 80 | }) 81 | 82 | deposit = BankStatement() 83 | deposit.bank_info = sender.bankinformation 84 | deposit.sender = sender 85 | deposit.receiver = sender 86 | deposit.amount = deposit_amount 87 | deposit.is_debit = True 88 | deposit.description = 'Amount withdrawn' 89 | deposit.save() 90 | 91 | serializer = TransactionSerializer(instance=deposit) 92 | return serializer.data 93 | 94 | 95 | class TransferTransactionSerializer(serializers.Serializer): 96 | sender = serializers.PrimaryKeyRelatedField( 97 | queryset=Customer.objects.filter(is_deleted=False), 98 | ) 99 | destination_account_number = serializers.CharField() 100 | amount = serializers.DecimalField(max_digits=12, decimal_places=2) 101 | 102 | @transaction.atomic 103 | def create(self, validated_data): 104 | sender = validated_data.get('sender') 105 | amount = validated_data.get('amount') 106 | account_number = validated_data.get('destination_account_number') 107 | 108 | try: 109 | receiver_bank = BankInformation.objects.select_for_update().get( 110 | account_number=account_number, 111 | ) 112 | except BankInformation.DoesNotExist: 113 | raise serializers.ValidationError({ 114 | 'destination_account_number': 'Invalid account number.' 115 | }) 116 | 117 | if not receiver_bank.is_active: 118 | raise serializers.ValidationError({ 119 | 'receiver': 'Bank account is blocked or inactive.' 120 | }) 121 | 122 | if not sender.bankinformation.is_active: 123 | raise serializers.ValidationError({ 124 | 'sender': 'Bank account is blocked or inactive.' 125 | }) 126 | 127 | # Ensure sender bank has sufficient balance. 128 | sender_bank = BankInformation.objects.select_for_update().get( 129 | pk=sender.bankinformation.pk, 130 | ) 131 | if sender_bank.total_balance < amount: 132 | raise serializers.ValidationError({ 133 | 'amount': 'Insufficient funds.' 134 | }) 135 | 136 | # Bank statement for sender. 137 | statement_sender = BankStatement() 138 | statement_sender.bank_info = sender.bankinformation 139 | statement_sender.sender = sender 140 | statement_sender.receiver = receiver_bank.holder 141 | statement_sender.amount = amount 142 | statement_sender.is_debit = True 143 | statement_sender.description = 'Amount transferred' 144 | statement_sender.save() 145 | 146 | # Bank statement for receiver. 147 | statement_receiver = BankStatement() 148 | statement_receiver.bank_info = receiver_bank 149 | statement_receiver.sender = sender 150 | statement_receiver.receiver = receiver_bank.holder 151 | statement_receiver.amount = amount 152 | statement_receiver.is_debit = False 153 | statement_receiver.description = 'Amount received' 154 | statement_receiver.save() 155 | 156 | serializer = TransactionSerializer(instance=statement_sender) 157 | return serializer.data 158 | 159 | 160 | class MutationSerializer(serializers.ModelSerializer): 161 | sender = serializers.CharField(source='bank_info.account_number') 162 | status = serializers.SerializerMethodField() 163 | 164 | def get_status(self, obj): 165 | return 'Debit' if obj.is_debit else 'Credit' 166 | 167 | class Meta: 168 | model = BankStatement 169 | fields = [ 170 | 'id', 'created', 'amount', 'status', 'sender', 'description' 171 | ] 172 | -------------------------------------------------------------------------------- /Simple BANK.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "3ef1be49-abfc-42aa-a2a2-c52ad4fedf95", 4 | "name": "Simple BANK", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "customer", 10 | "item": [ 11 | { 12 | "name": "Register Customer", 13 | "request": { 14 | "method": "POST", 15 | "header": [ 16 | { 17 | "key": "Content-Type", 18 | "value": "application/json" 19 | } 20 | ], 21 | "body": { 22 | "mode": "raw", 23 | "raw": "{\n \"address\": \"Jakarta timur jauh\",\n \"sex\": \"male\",\n \"identity_number\": \"123456789\",\n \"first_name\": \"adiyat\",\n \"last_name\": \"mubarak\",\n \"email\": \"adiyatmubarak@gmail.com\",\n \"password\": \"testing\"\n}" 24 | }, 25 | "url": { 26 | "raw": "127.0.0.1/api/v1/customers/", 27 | "host": [ 28 | "127", 29 | "0", 30 | "0", 31 | "1" 32 | ], 33 | "path": [ 34 | "api", 35 | "v1", 36 | "customers", 37 | "" 38 | ] 39 | } 40 | }, 41 | "response": [] 42 | }, 43 | { 44 | "name": "Get Customer Information", 45 | "request": { 46 | "auth": { 47 | "type": "bearer", 48 | "bearer": [ 49 | { 50 | "key": "token", 51 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzNjg5OTIzLCJqdGkiOiIyNzc1NjRjZTkwYjY0YTZlOWE2ZTRhZGI0YzA1MTU5ZSIsInVzZXJfaWQiOjJ9.pIHqT4K9WzlcW55MNM1tSm0AiyZeeCAq5Yw2Rhc4HDs", 52 | "type": "string" 53 | } 54 | ] 55 | }, 56 | "method": "GET", 57 | "header": [], 58 | "body": { 59 | "mode": "raw", 60 | "raw": "" 61 | }, 62 | "url": { 63 | "raw": "127.0.0.1/api/v1/customers/", 64 | "host": [ 65 | "127", 66 | "0", 67 | "0", 68 | "1" 69 | ], 70 | "path": [ 71 | "api", 72 | "v1", 73 | "customers", 74 | "" 75 | ] 76 | } 77 | }, 78 | "response": [] 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "authorization", 84 | "item": [ 85 | { 86 | "name": "Obtain JWT Token", 87 | "request": { 88 | "method": "POST", 89 | "header": [], 90 | "body": { 91 | "mode": "formdata", 92 | "formdata": [ 93 | { 94 | "key": "username", 95 | "value": "adiyatmubarak@gmail.com", 96 | "type": "text" 97 | }, 98 | { 99 | "key": "password", 100 | "value": "testing", 101 | "type": "text" 102 | } 103 | ] 104 | }, 105 | "url": { 106 | "raw": "http://127.0.0.1/token/", 107 | "protocol": "http", 108 | "host": [ 109 | "127", 110 | "0", 111 | "0", 112 | "1" 113 | ], 114 | "path": [ 115 | "token", 116 | "" 117 | ] 118 | } 119 | }, 120 | "response": [] 121 | } 122 | ] 123 | }, 124 | { 125 | "name": "accounts", 126 | "item": [ 127 | { 128 | "name": "Retrieve Bank Account", 129 | "request": { 130 | "auth": { 131 | "type": "bearer", 132 | "bearer": [ 133 | { 134 | "key": "token", 135 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzNjg5OTIzLCJqdGkiOiIyNzc1NjRjZTkwYjY0YTZlOWE2ZTRhZGI0YzA1MTU5ZSIsInVzZXJfaWQiOjJ9.pIHqT4K9WzlcW55MNM1tSm0AiyZeeCAq5Yw2Rhc4HDs", 136 | "type": "string" 137 | } 138 | ] 139 | }, 140 | "method": "GET", 141 | "header": [], 142 | "body": { 143 | "mode": "raw", 144 | "raw": "" 145 | }, 146 | "url": { 147 | "raw": "localhost/api/v1/accounts/", 148 | "host": [ 149 | "localhost" 150 | ], 151 | "path": [ 152 | "api", 153 | "v1", 154 | "accounts", 155 | "" 156 | ] 157 | } 158 | }, 159 | "response": [] 160 | }, 161 | { 162 | "name": "Bank Account Activation", 163 | "request": { 164 | "auth": { 165 | "type": "bearer", 166 | "bearer": [ 167 | { 168 | "key": "token", 169 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzNjg5OTIzLCJqdGkiOiIyNzc1NjRjZTkwYjY0YTZlOWE2ZTRhZGI0YzA1MTU5ZSIsInVzZXJfaWQiOjJ9.pIHqT4K9WzlcW55MNM1tSm0AiyZeeCAq5Yw2Rhc4HDs", 170 | "type": "string" 171 | } 172 | ] 173 | }, 174 | "method": "PUT", 175 | "header": [], 176 | "body": { 177 | "mode": "raw", 178 | "raw": "" 179 | }, 180 | "url": { 181 | "raw": "localhost/api/v1/accounts/4401fb05-3ffa-4530-b5c8-07f06b191254/activate/", 182 | "host": [ 183 | "localhost" 184 | ], 185 | "path": [ 186 | "api", 187 | "v1", 188 | "accounts", 189 | "4401fb05-3ffa-4530-b5c8-07f06b191254", 190 | "activate", 191 | "" 192 | ] 193 | } 194 | }, 195 | "response": [] 196 | }, 197 | { 198 | "name": "Bank deposit", 199 | "request": { 200 | "auth": { 201 | "type": "bearer", 202 | "bearer": [ 203 | { 204 | "key": "token", 205 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzNzI1NTM2LCJqdGkiOiJmY2EwODZjNzhiMjY0MDFkODkzYjA1ZjQ2YTZhZTc2NSIsInVzZXJfaWQiOjJ9.KXvRTY0dEj_5sN8oHfrjDK53WqqoF0wmMMo5jfYMPd0", 206 | "type": "string" 207 | } 208 | ] 209 | }, 210 | "method": "POST", 211 | "header": [ 212 | { 213 | "key": "Content-Type", 214 | "name": "Content-Type", 215 | "value": "application/json", 216 | "type": "text" 217 | } 218 | ], 219 | "body": { 220 | "mode": "raw", 221 | "raw": "{\n\t\"amount\": 145000\n}" 222 | }, 223 | "url": { 224 | "raw": "localhost/api/v1/accounts/deposit/", 225 | "host": [ 226 | "localhost" 227 | ], 228 | "path": [ 229 | "api", 230 | "v1", 231 | "accounts", 232 | "deposit", 233 | "" 234 | ] 235 | } 236 | }, 237 | "response": [] 238 | }, 239 | { 240 | "name": "Bank Transfer", 241 | "request": { 242 | "auth": { 243 | "type": "bearer", 244 | "bearer": [ 245 | { 246 | "key": "token", 247 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzNzcyMzAwLCJqdGkiOiJmZGVhNGUwZDRhNmU0N2QwOWNiNzgxNTBmZjUwZmMzYiIsInVzZXJfaWQiOjN9.IZIAocWY9FX0rSGV_zi_9ZCjERQHYl5CQX8yLF8smfE", 248 | "type": "string" 249 | } 250 | ] 251 | }, 252 | "method": "POST", 253 | "header": [ 254 | { 255 | "key": "Content-Type", 256 | "name": "Content-Type", 257 | "value": "application/json", 258 | "type": "text" 259 | } 260 | ], 261 | "body": { 262 | "mode": "raw", 263 | "raw": "{\n\t\"destination_account_number\": \"5F57CECF92C61\",\n\t\"amount\": 100000\n}" 264 | }, 265 | "url": { 266 | "raw": "localhost/api/v1/accounts/transfer/", 267 | "host": [ 268 | "localhost" 269 | ], 270 | "path": [ 271 | "api", 272 | "v1", 273 | "accounts", 274 | "transfer", 275 | "" 276 | ] 277 | } 278 | }, 279 | "response": [] 280 | }, 281 | { 282 | "name": "Bank Mutations", 283 | "request": { 284 | "auth": { 285 | "type": "bearer", 286 | "bearer": [ 287 | { 288 | "key": "token", 289 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzODgyMTgzLCJqdGkiOiI4NGRmODk2OTQxNWI0MWRkYWUxOWMzOTZlYTkwMjUyNSIsInVzZXJfaWQiOjN9.wbOyHtjD4eJ616X8rjO831TSF6U2ohDLZtB1J-fsq58", 290 | "type": "string" 291 | } 292 | ] 293 | }, 294 | "method": "GET", 295 | "header": [], 296 | "body": { 297 | "mode": "raw", 298 | "raw": "" 299 | }, 300 | "url": { 301 | "raw": "localhost/api/v1/accounts/ab619068-0ac8-4e0e-933e-01209694d7c9/mutations/", 302 | "host": [ 303 | "localhost" 304 | ], 305 | "path": [ 306 | "api", 307 | "v1", 308 | "accounts", 309 | "ab619068-0ac8-4e0e-933e-01209694d7c9", 310 | "mutations", 311 | "" 312 | ] 313 | } 314 | }, 315 | "response": [] 316 | }, 317 | { 318 | "name": "Bank Withdraw", 319 | "request": { 320 | "auth": { 321 | "type": "bearer", 322 | "bearer": [ 323 | { 324 | "key": "token", 325 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTQzODgyNTgyLCJqdGkiOiI3YzRmMTViYTU1MDY0NTAzYjBjZjBmYWViZDVhYmY2OSIsInVzZXJfaWQiOjN9.7d2Eifhir6q-4PvfBvVTkrroLLk6u1xqCYI20yBnVXo", 326 | "type": "string" 327 | } 328 | ] 329 | }, 330 | "method": "POST", 331 | "header": [ 332 | { 333 | "key": "Content-Type", 334 | "name": "Content-Type", 335 | "value": "application/json", 336 | "type": "text" 337 | } 338 | ], 339 | "body": { 340 | "mode": "raw", 341 | "raw": "{\n\t\"amount\": 20000\n}" 342 | }, 343 | "url": { 344 | "raw": "localhost/api/v1/accounts/withdraw/", 345 | "host": [ 346 | "localhost" 347 | ], 348 | "path": [ 349 | "api", 350 | "v1", 351 | "accounts", 352 | "withdraw", 353 | "" 354 | ] 355 | } 356 | }, 357 | "response": [] 358 | } 359 | ] 360 | } 361 | ] 362 | } -------------------------------------------------------------------------------- /src/administrations/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import status 3 | from rest_framework.reverse import reverse 4 | from rest_framework.test import APITestCase, APIRequestFactory, force_authenticate 5 | 6 | from customers.models import Customer 7 | from .models import BankInformation, BankStatement 8 | from .views import BankInformationViewSet, DepositViewSet, TransferViewSet, WithdrawViewSet 9 | 10 | 11 | class BankInformationAPITest(APITestCase): 12 | 13 | @classmethod 14 | def register_customer(cls, email, identity_id): 15 | user = User.objects.create_user( 16 | username=email, 17 | email=email, 18 | password='testing', 19 | first_name='tester', 20 | last_name='quality', 21 | ) 22 | 23 | customer = Customer.objects.create( 24 | identity_number=identity_id, 25 | address='Jakarta', 26 | sex=Customer.MALE, 27 | user=user, 28 | ) 29 | 30 | BankInformation.objects.create( 31 | account_number=BankInformation.generate_account_number(), 32 | holder=customer, 33 | ) 34 | 35 | @classmethod 36 | def create_deposit(cls, bank, amount): 37 | deposit = BankStatement() 38 | deposit.bank_info = bank 39 | deposit.sender = bank.holder 40 | deposit.receiver = bank.holder 41 | deposit.amount = amount 42 | deposit.is_debit = False 43 | deposit.description = 'Amount deposit' 44 | deposit.save() 45 | 46 | def setUp(self): 47 | self.factory = APIRequestFactory() 48 | 49 | def test_retrieve_rekening_information(self): 50 | self.register_customer('tester@gmail.com', '123456') 51 | customer = Customer.objects.get(user__username='tester@gmail.com') 52 | 53 | url = reverse('v1:accounts:bankinformation-list') 54 | request = self.factory.get(path=url) 55 | force_authenticate(request, customer.user) 56 | view = BankInformationViewSet.as_view({'get': 'list'}) 57 | response = view(request) 58 | self.assertEqual(response.status_code, status.HTTP_200_OK) 59 | 60 | def test_rekening_activation_another_customer(self): 61 | self.register_customer('tester1@gmail.com', '123456') 62 | customer1 = Customer.objects.get(user__username='tester1@gmail.com') 63 | bank_customer1 = customer1.bankinformation 64 | 65 | self.register_customer('tester2@gmail.com', '123457') 66 | customer2 = Customer.objects.get(user__username='tester2@gmail.com') 67 | 68 | url = reverse('v1:accounts:bankinformation-activate', args=[bank_customer1.guid]) 69 | request = self.factory.put(path=url) 70 | force_authenticate(request, customer2.user) 71 | view = BankInformationViewSet.as_view({'put': 'activate'}) 72 | response = view(request, guid=bank_customer1.guid) 73 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 74 | 75 | def test_rekening_activation(self): 76 | self.register_customer('tester@gmail.com', '123456') 77 | customer = Customer.objects.get(user__username='tester@gmail.com') 78 | bank_info = customer.bankinformation 79 | 80 | url = reverse('v1:accounts:bankinformation-activate', args=[bank_info.guid]) 81 | request = self.factory.put(path=url) 82 | force_authenticate(request, customer.user) 83 | view = BankInformationViewSet.as_view({'put': 'activate'}) 84 | response = view(request, guid=bank_info.guid) 85 | bank_info.refresh_from_db() 86 | self.assertEqual(response.status_code, status.HTTP_200_OK) 87 | self.assertTrue(bank_info.is_active) 88 | 89 | def test_block_rekening(self): 90 | self.register_customer('tester@gmail.com', '123456') 91 | customer = Customer.objects.get(user__username='tester@gmail.com') 92 | bank_info = customer.bankinformation 93 | bank_info.is_active = True 94 | bank_info.save() 95 | 96 | url = reverse('v1:accounts:bankinformation-deactivate', args=[bank_info.guid]) 97 | request = self.factory.put(path=url) 98 | force_authenticate(request, customer.user) 99 | view = BankInformationViewSet.as_view({'put': 'deactivate'}) 100 | response = view(request, guid=bank_info.guid) 101 | bank_info.refresh_from_db() 102 | self.assertEqual(response.status_code, status.HTTP_200_OK) 103 | self.assertFalse(bank_info.is_active) 104 | 105 | def test_unblock_rekening(self): 106 | self.register_customer('tester@gmail.com', '123456') 107 | customer = Customer.objects.get(user__username='tester@gmail.com') 108 | bank_info = customer.bankinformation 109 | bank_info.is_active = False 110 | bank_info.save() 111 | 112 | url = reverse('v1:accounts:bankinformation-activate', args=[bank_info.guid]) 113 | request = self.factory.put(path=url) 114 | force_authenticate(request, customer.user) 115 | view = BankInformationViewSet.as_view({'put': 'activate'}) 116 | response = view(request, guid=bank_info.guid) 117 | bank_info.refresh_from_db() 118 | self.assertEqual(response.status_code, status.HTTP_200_OK) 119 | self.assertTrue(bank_info.is_active) 120 | 121 | def test_deposit_rekening(self): 122 | self.register_customer('tester@gmail.com', '123456') 123 | customer = Customer.objects.get(user__username='tester@gmail.com') 124 | bank_info = customer.bankinformation 125 | bank_info.is_active = True 126 | bank_info.save() 127 | 128 | url = reverse('v1:accounts:deposit-list') 129 | data = {'amount': 20000} 130 | request = self.factory.post(path=url, data=data) 131 | force_authenticate(request, customer.user) 132 | view = DepositViewSet.as_view({'post': 'create'}) 133 | response = view(request) 134 | bank_info.refresh_from_db() 135 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 136 | self.assertEqual(bank_info.total_balance, 20000) 137 | mutation = bank_info.mutations.first() 138 | self.assertEqual(mutation.description, 'Amount deposit') 139 | 140 | def test_deposit_inactive_rekening(self): 141 | self.register_customer('tester@gmail.com', '123456') 142 | customer = Customer.objects.get(user__username='tester@gmail.com') 143 | 144 | url = reverse('v1:accounts:deposit-list') 145 | data = {'amount': 20000} 146 | request = self.factory.post(path=url, data=data) 147 | force_authenticate(request, customer.user) 148 | view = DepositViewSet.as_view({'post': 'create'}) 149 | response = view(request) 150 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 151 | self.assertEqual(str(response.data['sender'][0]), 'Bank account is blocked or inactive.') 152 | 153 | def test_withdraw_rekening(self): 154 | self.register_customer('customer1@gmail.com', '12345') 155 | customer = Customer.objects.get(user__email='customer1@gmail.com') 156 | bank_info = customer.bankinformation 157 | bank_info.is_active = True 158 | bank_info.save() 159 | self.create_deposit(bank_info, 100) 160 | 161 | data = { 162 | 'sender': customer.pk, 163 | 'amount': 10, 164 | } 165 | url = reverse('v1:administrations:withdraw-list') 166 | request = self.factory.post(path=url, data=data) 167 | force_authenticate(request, customer.user) 168 | view = WithdrawViewSet.as_view({'post': 'create'}) 169 | response = view(request) 170 | bank_info.refresh_from_db() 171 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 172 | self.assertEqual(bank_info.total_balance, 90) 173 | mutation = bank_info.mutations.latest('pk') 174 | self.assertEqual(mutation.description, 'Amount withdrawn') 175 | 176 | def test_withdraw_from_inactive_rekening(self): 177 | self.register_customer('customer1@gmail.com', '12345') 178 | customer = Customer.objects.get(user__email='customer1@gmail.com') 179 | bank_info = customer.bankinformation 180 | bank_info.is_active = False 181 | bank_info.save() 182 | self.create_deposit(bank_info, 100) 183 | 184 | data = { 185 | 'sender': customer.pk, 186 | 'amount': 10, 187 | } 188 | url = reverse('v1:administrations:withdraw-list') 189 | request = self.factory.post(path=url, data=data) 190 | force_authenticate(request, customer.user) 191 | view = WithdrawViewSet.as_view({'post': 'create'}) 192 | response = view(request) 193 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 194 | self.assertEqual(str(response.data['sender'][0]), 'Bank account is blocked or inactive.') 195 | 196 | def test_withdraw_rekening_with_amount_exceeded(self): 197 | self.register_customer('customer1@gmail.com', '12345') 198 | customer = Customer.objects.get(user__email='customer1@gmail.com') 199 | bank_info = customer.bankinformation 200 | bank_info.is_active = True 201 | bank_info.save() 202 | self.create_deposit(bank_info, 100) 203 | 204 | data = { 205 | 'sender': customer.pk, 206 | 'amount': 500, 207 | } 208 | url = reverse('v1:administrations:withdraw-list') 209 | request = self.factory.post(path=url, data=data) 210 | force_authenticate(request, customer.user) 211 | view = WithdrawViewSet.as_view({'post': 'create'}) 212 | response = view(request) 213 | bank_info.refresh_from_db() 214 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 215 | self.assertEqual(str(response.data['amount']), 'Insufficient funds.') 216 | 217 | def test_bank_transfer(self): 218 | self.register_customer('customer1@gmail.com', '12345') 219 | customer1 = Customer.objects.get(user__email='customer1@gmail.com') 220 | bank_customer1 = customer1.bankinformation 221 | bank_customer1.is_active = True 222 | bank_customer1.save() 223 | self.create_deposit(bank_customer1, 130000) 224 | 225 | self.register_customer('customer2@gmail.com', '54321') 226 | customer2 = Customer.objects.get(user__email='customer2@gmail.com') 227 | bank_customer2 = customer2.bankinformation 228 | bank_customer2.is_active = True 229 | bank_customer2.save() 230 | 231 | data = { 232 | 'destination_account_number': bank_customer2.account_number, 233 | 'amount': 30000 234 | } 235 | url = reverse('v1:administrations:transfer-list') 236 | request = self.factory.post(path=url, data=data) 237 | force_authenticate(request, customer1.user) 238 | view = TransferViewSet.as_view({'post': 'create'}) 239 | response = view(request) 240 | bank_customer1.refresh_from_db() 241 | bank_customer2.refresh_from_db() 242 | mutation_customer1 = bank_customer1.mutations.latest('pk') 243 | mutation_customer2 = bank_customer2.mutations.latest('pk') 244 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 245 | self.assertEqual(bank_customer1.total_balance, 100000) 246 | self.assertEqual(bank_customer2.total_balance, 30000) 247 | self.assertEqual(mutation_customer1.description, 'Amount transferred') 248 | self.assertEqual(mutation_customer2.description, 'Amount received') 249 | 250 | def test_bank_transfer_with_amount_exceeded(self): 251 | self.register_customer('customer1@gmail.com', '12345') 252 | customer1 = Customer.objects.get(user__email='customer1@gmail.com') 253 | bank_customer1 = customer1.bankinformation 254 | bank_customer1.is_active = True 255 | bank_customer1.save() 256 | self.create_deposit(bank_customer1, 1000) 257 | 258 | self.register_customer('customer2@gmail.com', '54321') 259 | customer2 = Customer.objects.get(user__email='customer2@gmail.com') 260 | bank_customer2 = customer2.bankinformation 261 | bank_customer2.is_active = True 262 | bank_customer2.save() 263 | 264 | data = { 265 | 'destination_account_number': bank_customer2.account_number, 266 | 'amount': 5000, 267 | } 268 | url = reverse('v1:administrations:transfer-list') 269 | request = self.factory.post(path=url, data=data) 270 | force_authenticate(request, customer1.user) 271 | view = TransferViewSet.as_view({'post': 'create'}) 272 | response = view(request) 273 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 274 | self.assertEqual(str(response.data['amount']), 'Insufficient funds.') 275 | 276 | def test_bank_transfer_using_inactive_rekening(self): 277 | self.register_customer('customer1@gmail.com', '12345') 278 | customer1 = Customer.objects.get(user__email='customer1@gmail.com') 279 | bank_customer1 = customer1.bankinformation 280 | bank_customer1.is_active = False 281 | bank_customer1.save() 282 | self.create_deposit(bank_customer1, 1000) 283 | 284 | self.register_customer('customer2@gmail.com', '54321') 285 | customer2 = Customer.objects.get(user__email='customer2@gmail.com') 286 | bank_customer2 = customer2.bankinformation 287 | bank_customer2.is_active = True 288 | bank_customer2.save() 289 | 290 | data = { 291 | 'destination_account_number': bank_customer2.account_number, 292 | 'amount': 5000, 293 | } 294 | url = reverse('v1:administrations:transfer-list') 295 | request = self.factory.post(path=url, data=data) 296 | force_authenticate(request, customer1.user) 297 | view = TransferViewSet.as_view({'post': 'create'}) 298 | response = view(request) 299 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 300 | self.assertEqual(str(response.data['sender']), 'Bank account is blocked or inactive.') 301 | 302 | def test_bank_transfer_to_inactive_rekening(self): 303 | self.register_customer('customer1@gmail.com', '12345') 304 | customer1 = Customer.objects.get(user__email='customer1@gmail.com') 305 | bank_customer1 = customer1.bankinformation 306 | bank_customer1.is_active = True 307 | bank_customer1.save() 308 | self.create_deposit(bank_customer1, 1000) 309 | 310 | self.register_customer('customer2@gmail.com', '54321') 311 | customer2 = Customer.objects.get(user__email='customer2@gmail.com') 312 | bank_customer2 = customer2.bankinformation 313 | bank_customer2.is_active = False 314 | bank_customer2.save() 315 | 316 | data = { 317 | 'destination_account_number': bank_customer2.account_number, 318 | 'amount': 1000, 319 | } 320 | url = reverse('v1:administrations:transfer-list') 321 | request = self.factory.post(path=url, data=data) 322 | force_authenticate(request, customer1.user) 323 | view = TransferViewSet.as_view({'post': 'create'}) 324 | response = view(request) 325 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 326 | self.assertEqual(str(response.data['receiver']), 'Bank account is blocked or inactive.') 327 | 328 | def test_bank_transfer_to_invalid_rekening(self): 329 | self.register_customer('customer1@gmail.com', '12345') 330 | customer1 = Customer.objects.get(user__email='customer1@gmail.com') 331 | bank_customer1 = customer1.bankinformation 332 | bank_customer1.is_active = True 333 | bank_customer1.save() 334 | self.create_deposit(bank_customer1, 1000) 335 | 336 | self.register_customer('customer2@gmail.com', '54321') 337 | customer2 = Customer.objects.get(user__email='customer2@gmail.com') 338 | bank_customer2 = customer2.bankinformation 339 | bank_customer2.is_active = True 340 | bank_customer2.save() 341 | 342 | data = { 343 | 'destination_account_number': 'HIYAHIYAHIYA', 344 | 'amount': 1000, 345 | } 346 | url = reverse('v1:administrations:transfer-list') 347 | request = self.factory.post(path=url, data=data) 348 | force_authenticate(request, customer1.user) 349 | view = TransferViewSet.as_view({'post': 'create'}) 350 | response = view(request) 351 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 352 | self.assertEqual(str(response.data['destination_account_number']), 'Invalid account number.') 353 | 354 | def test_retrieve_rekening_mutations(self): 355 | self.register_customer('customer1@gmail.com', '12345') 356 | customer = Customer.objects.get(user__email='customer1@gmail.com') 357 | bank_info = customer.bankinformation 358 | bank_info.is_active = True 359 | bank_info.save() 360 | self.create_deposit(bank_info, 1000) 361 | 362 | url = reverse( 363 | 'v1:administrations:bankinformation-mutations', 364 | args=[bank_info.guid.hex], 365 | ) 366 | request = self.factory.get(path=url) 367 | force_authenticate(request, customer.user) 368 | view = BankInformationViewSet.as_view({'get': 'mutations'}) 369 | response = view(request, guid=bank_info.guid) 370 | self.assertEqual(response.status_code, status.HTTP_200_OK) 371 | 372 | def test_retrieve_rekening_mutations_from_another_customer_account(self): 373 | self.register_customer('customer1@gmail.com', '12345') 374 | customer1 = Customer.objects.get(user__email='customer1@gmail.com') 375 | bank_customer1 = customer1.bankinformation 376 | bank_customer1.is_active = True 377 | bank_customer1.save() 378 | self.create_deposit(bank_customer1, 1000) 379 | 380 | self.register_customer('customer2@gmail.com', '54321') 381 | customer2 = Customer.objects.get(user__email='customer2@gmail.com') 382 | bank_customer2 = customer2.bankinformation 383 | bank_customer2.is_active = True 384 | bank_customer2.save() 385 | 386 | url = reverse( 387 | 'v1:administrations:bankinformation-mutations', 388 | args=[bank_customer1.guid.hex], 389 | ) 390 | request = self.factory.get(path=url) 391 | force_authenticate(request, customer2.user) 392 | view = BankInformationViewSet.as_view({'get': 'mutations'}) 393 | response = view(request, guid=bank_customer1.guid) 394 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 395 | --------------------------------------------------------------------------------