From 5f64f0d70cfc443f51b4d4c3e13bd6fc6d1b0f51 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Sat, 3 Dec 2022 09:34:20 -0400 Subject: [PATCH 01/12] initial commit --- README.md | 117 ++++++++++++++---------------------------------------- 1 file changed, 29 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index ea8115e67..54d5f5836 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,29 @@ -# Back End Test Project - -You should see this challenge as an opportunity to create an application following modern development best practices (given the stack of your choice), but also feel free to use your own architecture preferences (coding standards, code organization, third-party libraries, etc). It’s perfectly fine to use vanilla code or any framework or libraries. - -## Scope - -In this challenge you should build an API for an application that stores and manages investments, it should have the following features: - -1. __Creation__ of an investment with an owner, a creation date and an amount. - 1. The creation date of an investment can be today or a date in the past. - 2. An investment should not be or become negative. -2. __View__ of an investment with its initial amount and expected balance. - 1. Expected balance should be the sum of the invested amount and the [gains][]. - 2. If an investment was already withdrawn then the balance must reflect the gains of that investment -3. __Withdrawal__ of a investment. - 1. The withdraw will always be the sum of the initial amount and its gains, - partial withdrawn is not supported. - 2. Withdrawals can happen in the past or today, but can't happen before the investment creation or the future. - 3. [Taxes][taxes] need to be applied to the withdrawals before showing the final value. -4. __List__ of a person's investments - 1. This list should have pagination. - -__NOTE:__ the implementation of an interface will not be evaluated. - -### Gain Calculation - -The investment will pay 0.52% every month in the same day of the investment creation. - -Given that the gain is paid every month, it should be treated as [compound gain][], which means that every new period (month) the amount gained will become part of the investment balance for the next payment. - -### Taxation - -When money is withdrawn, tax is triggered. Taxes apply only to the profit/gain portion of the money withdrawn. For example, if the initial investment was 1000.00, the current balance is 1200.00, then the taxes will be applied to the 200.00. - -The tax percentage changes according to the age of the investment: -* If it is less than one year old, the percentage will be 22.5% (tax = 45.00). -* If it is between one and two years old, the percentage will be 18.5% (tax = 37.00). -* If older than two years, the percentage will be 15% (tax = 30.00). - -## Requirements -1. Create project using any technology of your preference. It’s perfectly OK to use vanilla code or any framework or libraries; -2. Although you can use as many dependencies as you want, you should manage them wisely; -3. It is not necessary to send the notification emails, however, the code required for that would be welcome; -4. The API must be documented in some way. - -## Deliverables -The project source code and dependencies should be made available in GitHub. Here are the steps you should follow: -1. Fork this repository to your GitHub account (create an account if you don't have one, you will need it working with us). -2. Create a "development" branch and commit the code to it. Do not push the code to the main branch. -3. Include a README file that describes: - - Special build instructions, if any - - List of third-party libraries used and short description of why/how they were used - - A link to the API documentation. -4. Once the work is complete, create a pull request from "development" into "main" and send us the link. -5. Avoid using huge commits hiding your progress. Feel free to work on a branch and use `git rebase` to adjust your commits before submitting the final version. - -## Coding Standards -When working on the project be as clean and consistent as possible. - -## Project Deadline -Ideally you'd finish the test project in 5 days. It shouldn't take you longer than a entire week. - -## Quality Assurance -Use the following checklist to ensure high quality of the project. - -### General -- First of all, the application should run without errors. -- Are all requirements set above met? -- Is coding style consistent? -- The API is well documented? -- The API has unit tests? - -## Submission -1. A link to the Github repository. -2. Briefly describe how you decided on the tools that you used. - -## Have Fun Coding 🤘 -- This challenge description is intentionally vague in some aspects, but if you need assistance feel free to ask for help. -- If any of the seems out of your current level, you may skip it, but remember to tell us about it in the pull request. - -## Credits - -This coding challenge was inspired on [kinvoapp/kinvo-back-end-test](https://github.com/kinvoapp/kinvo-back-end-test/blob/2f17d713de739e309d17a1a74a82c3fd0e66d128/README.md) - -[gains]: #gain-calculation -[taxes]: #taxation -[interest]: #interest-calculation -[compound gain]: https://www.investopedia.com/terms/g/gain.asp +Neste desafio deverás construir uma API para uma aplicação que armazena e gere investimentos, a mesma deverá ter as seguintes funcionalidades: + +Criação de um investimento com proprietário, data de criação e valor. +A data de criação de um investimento pode ser hoje ou no passado. +Um investimento não deve ser ou se tornar negativo. +Visualização de um investimento com seu valor inicial e saldo esperado. +O saldo esperado deve ser a soma do valor investido e dos ganhos. +Se um investimento já foi retirado, o saldo deve refletir os ganhos desse investimento +Retirada de um investimento. +A retirada será sempre a soma do valor inicial e seus ganhos, a retirada parcial não é suportada. +As retiradas podem acontecer no passado ou hoje, mas não podem acontecer antes da criação do investimento ou no futuro. +Os impostos precisam ser aplicados aos saques antes de mostrar o valor final. +Lista de investimentos de uma pessoa +Esta lista deve ter paginação. +NOTA: a implementação de uma interface não será avaliada. + +Cálculo de ganho +O investimento pagará 0,52% todo mês no mesmo dia da criação do investimento. + +Tendo em vista que o ganho é pago mensalmente, ele deve ser tratado como ganho composto, ou seja, a cada novo período (mês) o valor ganho passará a fazer parte do saldo do investimento para o próximo pagamento. + +Tributação +Quando o dinheiro é retirado, o imposto é acionado. Os impostos se aplicam apenas à parte do lucro/ganho do dinheiro sacado. Por exemplo, se o investimento inicial foi de 1.000,00, o saldo atual é de 1.200,00, então os impostos incidirão sobre os 200,00. + +A porcentagem de imposto muda de acordo com a idade do investimento: + +Se tiver menos de um ano, o percentual será de 22,5% (taxa = 45,00). +Se tiver entre um e dois anos, o percentual será de 18,5% (taxa = 37,00). +Se for maior de dois anos, o percentual será de 15% (taxa = 30,00). \ No newline at end of file From 493fc297fe22e99b90a111a5bc2859a101d53529 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Sat, 3 Dec 2022 09:55:08 -0400 Subject: [PATCH 02/12] feat: initial config and apps created --- .env.example | 4 + .gitignore | 6 ++ README.md | 117 +++++++++++++++++------ investments/__init__.py | 0 investments/admin.py | 3 + investments/apps.py | 6 ++ investments/migrations/__init__.py | 0 investments/models.py | 3 + investments/tests.py | 3 + investments/views.py | 3 + investments_management/__init__.py | 0 investments_management/asgi.py | 16 ++++ investments_management/settings.py | 145 +++++++++++++++++++++++++++++ investments_management/urls.py | 21 +++++ investments_management/wsgi.py | 16 ++++ manage.py | 22 +++++ requirements.txt | 25 +++++ users/__init__.py | 0 users/admin.py | 3 + users/apps.py | 6 ++ users/migrations/__init__.py | 0 users/models.py | 6 ++ users/tests.py | 3 + users/views.py | 3 + 24 files changed, 382 insertions(+), 29 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 investments/__init__.py create mode 100644 investments/admin.py create mode 100644 investments/apps.py create mode 100644 investments/migrations/__init__.py create mode 100644 investments/models.py create mode 100644 investments/tests.py create mode 100644 investments/views.py create mode 100644 investments_management/__init__.py create mode 100644 investments_management/asgi.py create mode 100644 investments_management/settings.py create mode 100644 investments_management/urls.py create mode 100644 investments_management/wsgi.py create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 users/__init__.py create mode 100644 users/admin.py create mode 100644 users/apps.py create mode 100644 users/migrations/__init__.py create mode 100644 users/models.py create mode 100644 users/tests.py create mode 100644 users/views.py diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..579bbb7b2 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +SECRET_KEY= +POSTGRES_DB= +POSTGRES_USER= +POSTGRES_PASSWORD= diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..9b3f0c40d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +venv +__pycache__ +.env +db.sqlite3 +.vscode +.coverage diff --git a/README.md b/README.md index 54d5f5836..ea8115e67 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,88 @@ -Neste desafio deverás construir uma API para uma aplicação que armazena e gere investimentos, a mesma deverá ter as seguintes funcionalidades: - -Criação de um investimento com proprietário, data de criação e valor. -A data de criação de um investimento pode ser hoje ou no passado. -Um investimento não deve ser ou se tornar negativo. -Visualização de um investimento com seu valor inicial e saldo esperado. -O saldo esperado deve ser a soma do valor investido e dos ganhos. -Se um investimento já foi retirado, o saldo deve refletir os ganhos desse investimento -Retirada de um investimento. -A retirada será sempre a soma do valor inicial e seus ganhos, a retirada parcial não é suportada. -As retiradas podem acontecer no passado ou hoje, mas não podem acontecer antes da criação do investimento ou no futuro. -Os impostos precisam ser aplicados aos saques antes de mostrar o valor final. -Lista de investimentos de uma pessoa -Esta lista deve ter paginação. -NOTA: a implementação de uma interface não será avaliada. - -Cálculo de ganho -O investimento pagará 0,52% todo mês no mesmo dia da criação do investimento. - -Tendo em vista que o ganho é pago mensalmente, ele deve ser tratado como ganho composto, ou seja, a cada novo período (mês) o valor ganho passará a fazer parte do saldo do investimento para o próximo pagamento. - -Tributação -Quando o dinheiro é retirado, o imposto é acionado. Os impostos se aplicam apenas à parte do lucro/ganho do dinheiro sacado. Por exemplo, se o investimento inicial foi de 1.000,00, o saldo atual é de 1.200,00, então os impostos incidirão sobre os 200,00. - -A porcentagem de imposto muda de acordo com a idade do investimento: - -Se tiver menos de um ano, o percentual será de 22,5% (taxa = 45,00). -Se tiver entre um e dois anos, o percentual será de 18,5% (taxa = 37,00). -Se for maior de dois anos, o percentual será de 15% (taxa = 30,00). \ No newline at end of file +# Back End Test Project + +You should see this challenge as an opportunity to create an application following modern development best practices (given the stack of your choice), but also feel free to use your own architecture preferences (coding standards, code organization, third-party libraries, etc). It’s perfectly fine to use vanilla code or any framework or libraries. + +## Scope + +In this challenge you should build an API for an application that stores and manages investments, it should have the following features: + +1. __Creation__ of an investment with an owner, a creation date and an amount. + 1. The creation date of an investment can be today or a date in the past. + 2. An investment should not be or become negative. +2. __View__ of an investment with its initial amount and expected balance. + 1. Expected balance should be the sum of the invested amount and the [gains][]. + 2. If an investment was already withdrawn then the balance must reflect the gains of that investment +3. __Withdrawal__ of a investment. + 1. The withdraw will always be the sum of the initial amount and its gains, + partial withdrawn is not supported. + 2. Withdrawals can happen in the past or today, but can't happen before the investment creation or the future. + 3. [Taxes][taxes] need to be applied to the withdrawals before showing the final value. +4. __List__ of a person's investments + 1. This list should have pagination. + +__NOTE:__ the implementation of an interface will not be evaluated. + +### Gain Calculation + +The investment will pay 0.52% every month in the same day of the investment creation. + +Given that the gain is paid every month, it should be treated as [compound gain][], which means that every new period (month) the amount gained will become part of the investment balance for the next payment. + +### Taxation + +When money is withdrawn, tax is triggered. Taxes apply only to the profit/gain portion of the money withdrawn. For example, if the initial investment was 1000.00, the current balance is 1200.00, then the taxes will be applied to the 200.00. + +The tax percentage changes according to the age of the investment: +* If it is less than one year old, the percentage will be 22.5% (tax = 45.00). +* If it is between one and two years old, the percentage will be 18.5% (tax = 37.00). +* If older than two years, the percentage will be 15% (tax = 30.00). + +## Requirements +1. Create project using any technology of your preference. It’s perfectly OK to use vanilla code or any framework or libraries; +2. Although you can use as many dependencies as you want, you should manage them wisely; +3. It is not necessary to send the notification emails, however, the code required for that would be welcome; +4. The API must be documented in some way. + +## Deliverables +The project source code and dependencies should be made available in GitHub. Here are the steps you should follow: +1. Fork this repository to your GitHub account (create an account if you don't have one, you will need it working with us). +2. Create a "development" branch and commit the code to it. Do not push the code to the main branch. +3. Include a README file that describes: + - Special build instructions, if any + - List of third-party libraries used and short description of why/how they were used + - A link to the API documentation. +4. Once the work is complete, create a pull request from "development" into "main" and send us the link. +5. Avoid using huge commits hiding your progress. Feel free to work on a branch and use `git rebase` to adjust your commits before submitting the final version. + +## Coding Standards +When working on the project be as clean and consistent as possible. + +## Project Deadline +Ideally you'd finish the test project in 5 days. It shouldn't take you longer than a entire week. + +## Quality Assurance +Use the following checklist to ensure high quality of the project. + +### General +- First of all, the application should run without errors. +- Are all requirements set above met? +- Is coding style consistent? +- The API is well documented? +- The API has unit tests? + +## Submission +1. A link to the Github repository. +2. Briefly describe how you decided on the tools that you used. + +## Have Fun Coding 🤘 +- This challenge description is intentionally vague in some aspects, but if you need assistance feel free to ask for help. +- If any of the seems out of your current level, you may skip it, but remember to tell us about it in the pull request. + +## Credits + +This coding challenge was inspired on [kinvoapp/kinvo-back-end-test](https://github.com/kinvoapp/kinvo-back-end-test/blob/2f17d713de739e309d17a1a74a82c3fd0e66d128/README.md) + +[gains]: #gain-calculation +[taxes]: #taxation +[interest]: #interest-calculation +[compound gain]: https://www.investopedia.com/terms/g/gain.asp diff --git a/investments/__init__.py b/investments/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/investments/admin.py b/investments/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/investments/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/investments/apps.py b/investments/apps.py new file mode 100644 index 000000000..310f067bc --- /dev/null +++ b/investments/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class InvestmentsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "investments" diff --git a/investments/migrations/__init__.py b/investments/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/investments/models.py b/investments/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/investments/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/investments/tests.py b/investments/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/investments/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/investments/views.py b/investments/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/investments/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/investments_management/__init__.py b/investments_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/investments_management/asgi.py b/investments_management/asgi.py new file mode 100644 index 000000000..2d0efc648 --- /dev/null +++ b/investments_management/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for investments_management project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "investments_management.settings") + +application = get_asgi_application() diff --git a/investments_management/settings.py b/investments_management/settings.py new file mode 100644 index 000000000..666cb05a1 --- /dev/null +++ b/investments_management/settings.py @@ -0,0 +1,145 @@ +""" +Django settings for investments_management project. + +Generated by 'django-admin startproject' using Django 4.1.3. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +from pathlib import Path +import os +import dotenv +import dj_database_url + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +dotenv.load_dotenv() + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-dmck!nmk*spjrx&@verz(-=m3es9)8x)14*itm2mo-o7t5-4av" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +DJANGO_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +THIRD_PARTY_APPS = [ + "rest_framework", + "drf_spectacular", +] + +MY_APPS = [ + "investments" +] + +INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + MY_APPS + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "investments_management.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "investments_management.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +AUTH_USER_MODEL = "users.User" + +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 5, +} \ No newline at end of file diff --git a/investments_management/urls.py b/investments_management/urls.py new file mode 100644 index 000000000..71172c8c9 --- /dev/null +++ b/investments_management/urls.py @@ -0,0 +1,21 @@ +"""investments_management URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/investments_management/wsgi.py b/investments_management/wsgi.py new file mode 100644 index 000000000..8e57935c1 --- /dev/null +++ b/investments_management/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for investments_management project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "investments_management.settings") + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100644 index 000000000..adc87f117 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "investments_management.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..9ef78ab10 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,25 @@ +asgiref==3.5.2 +asttokens==2.2.0 +backcall==0.2.0 +colorama==0.4.6 +decorator==5.1.1 +Django==4.1.3 +djangorestframework==3.14.0 +executing==1.2.0 +ipdb==0.13.9 +ipython==8.7.0 +jedi==0.18.2 +matplotlib-inline==0.1.6 +parso==0.8.3 +pickleshare==0.7.5 +prompt-toolkit==3.0.33 +pure-eval==0.2.2 +Pygments==2.13.0 +pytz==2022.6 +six==1.16.0 +sqlparse==0.4.3 +stack-data==0.6.2 +toml==0.10.2 +traitlets==5.6.0 +tzdata==2022.7 +wcwidth==0.2.5 diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 000000000..88f7b1798 --- /dev/null +++ b/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/users/models.py b/users/models.py new file mode 100644 index 000000000..88e9a5b30 --- /dev/null +++ b/users/models.py @@ -0,0 +1,6 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +import uuid + +class User(AbstractUser): + ... diff --git a/users/tests.py b/users/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/users/views.py b/users/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/users/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 9cab6f7aa222bb8349cd6ed151dd73bc2fa01cab Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Sat, 3 Dec 2022 10:00:04 -0400 Subject: [PATCH 03/12] hotfix: remove django secret key and get from .env --- investments_management/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/investments_management/settings.py b/investments_management/settings.py index 666cb05a1..110c0d087 100644 --- a/investments_management/settings.py +++ b/investments_management/settings.py @@ -24,7 +24,7 @@ # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-dmck!nmk*spjrx&@verz(-=m3es9)8x)14*itm2mo-o7t5-4av" +SECRET_KEY = os.getenv('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True From 142196157c83c95ee1c72bfa16c62fb96f48c7f5 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Sun, 4 Dec 2022 17:01:52 -0400 Subject: [PATCH 04/12] feat: CRUD users and investments, can relate owner to a investment already --- investments/migrations/0001_initial.py | 30 ++++++ investments/migrations/0002_initial.py | 27 ++++++ investments/models.py | 20 +++- investments/serializers.py | 36 +++++++ investments/urls.py | 10 ++ investments/views.py | 36 ++++++- investments_management/settings.py | 5 +- investments_management/urls.py | 4 +- users/migrations/0001_initial.py | 128 +++++++++++++++++++++++++ users/models.py | 3 +- users/serializers.py | 33 +++++++ users/urls.py | 12 +++ users/views.py | 16 +++- 13 files changed, 351 insertions(+), 9 deletions(-) create mode 100644 investments/migrations/0001_initial.py create mode 100644 investments/migrations/0002_initial.py create mode 100644 investments/serializers.py create mode 100644 investments/urls.py create mode 100644 users/migrations/0001_initial.py create mode 100644 users/serializers.py create mode 100644 users/urls.py diff --git a/investments/migrations/0001_initial.py b/investments/migrations/0001_initial.py new file mode 100644 index 000000000..4c65c3587 --- /dev/null +++ b/investments/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.3 on 2022-12-04 14:49 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Investment", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("amount", models.PositiveIntegerField()), + ("created_at", models.DateField()), + ], + ), + ] diff --git a/investments/migrations/0002_initial.py b/investments/migrations/0002_initial.py new file mode 100644 index 000000000..35717c9e9 --- /dev/null +++ b/investments/migrations/0002_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.3 on 2022-12-04 14:49 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("investments", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="investment", + name="owner", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="investments", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/investments/models.py b/investments/models.py index 71a836239..cf7ce38c2 100644 --- a/investments/models.py +++ b/investments/models.py @@ -1,3 +1,21 @@ from django.db import models +from django.core.exceptions import ValidationError +import uuid -# Create your models here. +def validate_amount(value): + if value < 0 : + raise ValidationError( + ('%(value)s is a invalid number, please only positive numbers!'), + params={"value": value} + ) + +class Investment(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False) + amount = models.PositiveIntegerField(validators=[validate_amount]) + created_at = models.DateField() + + owner = models.ForeignKey( + "users.User", + on_delete=models.CASCADE, + related_name="investments" + ) diff --git a/investments/serializers.py b/investments/serializers.py new file mode 100644 index 000000000..e6f980387 --- /dev/null +++ b/investments/serializers.py @@ -0,0 +1,36 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import serializers + +from investments.models import Investment +from users.models import User +import ipdb + +class InvestmentSerializer(serializers.ModelSerializer): + class Meta: + model = Investment + + fields =[ + "id", + "amount", + "created_at", + "owner_id" + ] + + read_only_fields=["id", "owner_id"] + +class InvestmentDetailSerializer(serializers.ModelSerializer): + + class Meta: + model = Investment + + fields = [ + "id", + "amount", + "created_at", + "owner" + ] + + read_only_fields=[ + "id", + "owner" + ] \ No newline at end of file diff --git a/investments/urls.py b/investments/urls.py new file mode 100644 index 000000000..73c57269e --- /dev/null +++ b/investments/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from investments.views import CreateInvestmentView, ListInvestmentView, RetrieveUpdateDestroyInvestmentDetailView + + +urlpatterns=[ + path('investments/', ListInvestmentView.as_view()), + path('investments//', CreateInvestmentView.as_view()), + path('investment//', RetrieveUpdateDestroyInvestmentDetailView.as_view()) +] diff --git a/investments/views.py b/investments/views.py index 91ea44a21..6d793d6b1 100644 --- a/investments/views.py +++ b/investments/views.py @@ -1,3 +1,35 @@ -from django.shortcuts import render +from django.shortcuts import get_object_or_404 +from rest_framework import generics +import ipdb -# Create your views here. +from investments.models import Investment +from investments.serializers import InvestmentDetailSerializer, InvestmentSerializer +from users.models import User + +class ListInvestmentView(generics.ListAPIView): + queryset = Investment.objects.all() + serializer_class = InvestmentSerializer + + +class CreateInvestmentView(generics.CreateAPIView): + + queryset = Investment.objects.all() + serializer_class = InvestmentDetailSerializer + + lookup_url_kwarg = 'owner_id' + + def perform_create(self, serializer): + owner_id = self.kwargs['owner_id'] + + owner = get_object_or_404(User, pk=owner_id) + + serializer.save(owner=owner) + +class RetrieveUpdateDestroyInvestmentDetailView( + generics.RetrieveUpdateDestroyAPIView + ): + + queryset = Investment.objects.all() + serializer_class = InvestmentDetailSerializer + + lookup_url_kwarg = 'investment_id' \ No newline at end of file diff --git a/investments_management/settings.py b/investments_management/settings.py index 110c0d087..6b5118925 100644 --- a/investments_management/settings.py +++ b/investments_management/settings.py @@ -13,7 +13,7 @@ from pathlib import Path import os import dotenv -import dj_database_url + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -49,7 +49,8 @@ ] MY_APPS = [ - "investments" + "investments", + "users" ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + MY_APPS diff --git a/investments_management/urls.py b/investments_management/urls.py index 71172c8c9..c59bf5a36 100644 --- a/investments_management/urls.py +++ b/investments_management/urls.py @@ -14,8 +14,10 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), + path("api/", include("users.urls")), + path("api/", include("investments.urls")) ] diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 000000000..563b2668a --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,128 @@ +# Generated by Django 4.1.3 on 2022-12-04 14:49 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "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", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("email", models.CharField(max_length=127)), + ( + "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", + ), + ), + ( + "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", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/users/models.py b/users/models.py index 88e9a5b30..96528431e 100644 --- a/users/models.py +++ b/users/models.py @@ -3,4 +3,5 @@ import uuid class User(AbstractUser): - ... + id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False) + email = models.CharField(max_length=127) diff --git a/users/serializers.py b/users/serializers.py new file mode 100644 index 000000000..b28ba403a --- /dev/null +++ b/users/serializers.py @@ -0,0 +1,33 @@ +from rest_framework import serializers +from rest_framework.exceptions import APIException + +from investments.serializers import InvestmentSerializer + +from users.models import User + +class Error(APIException): + status_code = 403 + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + + fields =[ + "id", + "email", + "username" + ] + + read_only_fields=["id"] + + def create(self, validated_data): + if User.objects.filter(email=validated_data["email"]).exists(): + raise Error({"message": "Email already been used!"}) + return User.objects.create_user(**validated_data) + +class UserDetailSerializer(serializers.ModelSerializer): + investments = InvestmentSerializer(many=True) + class Meta: + model = User + + fields = [ "username","email", "investments"] \ No newline at end of file diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 000000000..c636e847a --- /dev/null +++ b/users/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView + +from users.views import CreateUserView, ListUpdateDeleteDetailUserView + +urlpatterns=[ + path('accounts/', CreateUserView.as_view()), + path('accounts//', ListUpdateDeleteDetailUserView.as_view()), + path('schema/', SpectacularAPIView.as_view(), name='schema'), + path('docs/', SpectacularRedocView.as_view(url_name='schema'), name='redoc') +] diff --git a/users/views.py b/users/views.py index 91ea44a21..403673aa0 100644 --- a/users/views.py +++ b/users/views.py @@ -1,3 +1,15 @@ -from django.shortcuts import render +from rest_framework import generics -# Create your views here. +from users.models import User +from users.serializers import UserDetailSerializer, UserSerializer + +class CreateUserView(generics.ListCreateAPIView): + queryset= User.objects.all() + serializer_class= UserSerializer + +class ListUpdateDeleteDetailUserView(generics.RetrieveUpdateDestroyAPIView): + + queryset = User.objects.all() + serializer_class = UserDetailSerializer + + lookup_field = "id" From a0c47641bbab55eb9e048a40343c80997d94f297 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Sun, 4 Dec 2022 17:04:45 -0400 Subject: [PATCH 05/12] hotfix: applied migrations --- .../0003_alter_investment_amount.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 investments/migrations/0003_alter_investment_amount.py diff --git a/investments/migrations/0003_alter_investment_amount.py b/investments/migrations/0003_alter_investment_amount.py new file mode 100644 index 000000000..5f5eafaa3 --- /dev/null +++ b/investments/migrations/0003_alter_investment_amount.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.3 on 2022-12-04 21:04 + +from django.db import migrations, models +import investments.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("investments", "0002_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="investment", + name="amount", + field=models.PositiveIntegerField( + validators=[investments.models.validate_amount] + ), + ), + ] From 5b449016ce3fd051a90adcb026a2a544be9bd7a0 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Mon, 5 Dec 2022 19:28:31 -0400 Subject: [PATCH 06/12] fix: create gains and withdrawn_date to Investment Model --- investments/migrations/0001_initial.py | 19 +++++++++++++++-- investments/migrations/0002_initial.py | 4 ++-- .../0003_alter_investment_amount.py | 21 ------------------- investments/models.py | 8 ++++++- investments/serializers.py | 19 +++++++++++++---- investments/views.py | 7 ++++++- users/migrations/0001_initial.py | 2 +- 7 files changed, 48 insertions(+), 32 deletions(-) delete mode 100644 investments/migrations/0003_alter_investment_amount.py diff --git a/investments/migrations/0001_initial.py b/investments/migrations/0001_initial.py index 4c65c3587..a01b9fbe8 100644 --- a/investments/migrations/0001_initial.py +++ b/investments/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 4.1.3 on 2022-12-04 14:49 +# Generated by Django 4.1.3 on 2022-12-05 23:27 from django.db import migrations, models +import investments.models import uuid @@ -23,8 +24,22 @@ class Migration(migrations.Migration): serialize=False, ), ), - ("amount", models.PositiveIntegerField()), + ( + "amount", + models.DecimalField( + decimal_places=2, + max_digits=10, + validators=[investments.models.validate_amount], + ), + ), ("created_at", models.DateField()), + ( + "gains", + models.DecimalField( + decimal_places=2, default=0, max_digits=5, null=True + ), + ), + ("withdrawn_date", models.DateField(null=True)), ], ), ] diff --git a/investments/migrations/0002_initial.py b/investments/migrations/0002_initial.py index 35717c9e9..7d3fe06cb 100644 --- a/investments/migrations/0002_initial.py +++ b/investments/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-12-04 14:49 +# Generated by Django 4.1.3 on 2022-12-05 23:27 from django.conf import settings from django.db import migrations, models @@ -10,8 +10,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("investments", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("investments", "0001_initial"), ] operations = [ diff --git a/investments/migrations/0003_alter_investment_amount.py b/investments/migrations/0003_alter_investment_amount.py deleted file mode 100644 index 5f5eafaa3..000000000 --- a/investments/migrations/0003_alter_investment_amount.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.1.3 on 2022-12-04 21:04 - -from django.db import migrations, models -import investments.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("investments", "0002_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="investment", - name="amount", - field=models.PositiveIntegerField( - validators=[investments.models.validate_amount] - ), - ), - ] diff --git a/investments/models.py b/investments/models.py index cf7ce38c2..53d0b142b 100644 --- a/investments/models.py +++ b/investments/models.py @@ -11,11 +11,17 @@ def validate_amount(value): class Investment(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False) - amount = models.PositiveIntegerField(validators=[validate_amount]) + amount = models.DecimalField(decimal_places=2,max_digits=10,validators=[validate_amount]) created_at = models.DateField() + gains = models.DecimalField(decimal_places=2,max_digits=5, null=True, default=0) + withdrawn_date = models.DateField(null=True) owner = models.ForeignKey( "users.User", on_delete=models.CASCADE, related_name="investments" ) + +#Colocar gains, decimal e mudar amount +#Colocar withdrawnDate que começa vazio +#Caso tenha acontecido withdrawn colocar a data diff --git a/investments/serializers.py b/investments/serializers.py index e6f980387..09d2a74e8 100644 --- a/investments/serializers.py +++ b/investments/serializers.py @@ -13,10 +13,17 @@ class Meta: "id", "amount", "created_at", - "owner_id" + "gains", + "owner_id", + "withdrawn_date" ] - read_only_fields=["id", "owner_id"] + read_only_fields=[ + "id", + "owner_id", + "gains", + "withdrawn_date" + ] class InvestmentDetailSerializer(serializers.ModelSerializer): @@ -27,10 +34,14 @@ class Meta: "id", "amount", "created_at", - "owner" + "gains", + "owner", + "withdrawn_date" ] read_only_fields=[ "id", - "owner" + "owner", + "gains", + "withdrawn_date" ] \ No newline at end of file diff --git a/investments/views.py b/investments/views.py index 6d793d6b1..c52cd5064 100644 --- a/investments/views.py +++ b/investments/views.py @@ -32,4 +32,9 @@ class RetrieveUpdateDestroyInvestmentDetailView( queryset = Investment.objects.all() serializer_class = InvestmentDetailSerializer - lookup_url_kwarg = 'investment_id' \ No newline at end of file + lookup_url_kwarg = 'investment_id' + + def destroy(self, request, *args, **kwargs): + ... + +# mudar o delete para soft delete \ No newline at end of file diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index 563b2668a..f2c745529 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-12-04 14:49 +# Generated by Django 4.1.3 on 2022-12-05 23:27 import django.contrib.auth.models import django.contrib.auth.validators From 7417fe8da3bb7a5bcc91c5229f4f978d91cbaeb0 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Tue, 6 Dec 2022 14:52:05 -0400 Subject: [PATCH 07/12] feat: validators data working, compound gains its working, has validation to ages(-1, 1-2, 2+) --- investments/migrations/0001_initial.py | 18 ++- investments/migrations/0002_initial.py | 4 +- investments/models.py | 39 +++++- investments/serializers.py | 16 ++- investments/views.py | 174 ++++++++++++++++++++++++- investments_management/settings.py | 2 +- users/migrations/0001_initial.py | 2 +- 7 files changed, 242 insertions(+), 13 deletions(-) diff --git a/investments/migrations/0001_initial.py b/investments/migrations/0001_initial.py index a01b9fbe8..5cfa639cc 100644 --- a/investments/migrations/0001_initial.py +++ b/investments/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-12-05 23:27 +# Generated by Django 4.1.3 on 2022-12-06 18:44 from django.db import migrations, models import investments.models @@ -24,6 +24,10 @@ class Migration(migrations.Migration): serialize=False, ), ), + ( + "initial_amount", + models.DecimalField(decimal_places=2, max_digits=10, null=True), + ), ( "amount", models.DecimalField( @@ -32,7 +36,10 @@ class Migration(migrations.Migration): validators=[investments.models.validate_amount], ), ), - ("created_at", models.DateField()), + ( + "created_at", + models.DateField(validators=[investments.models.validate_date]), + ), ( "gains", models.DecimalField( @@ -40,6 +47,13 @@ class Migration(migrations.Migration): ), ), ("withdrawn_date", models.DateField(null=True)), + ( + "expected_balance", + models.DecimalField( + decimal_places=2, default=0, max_digits=10, null=True + ), + ), + ("isActive", models.BooleanField(default=True)), ], ), ] diff --git a/investments/migrations/0002_initial.py b/investments/migrations/0002_initial.py index 7d3fe06cb..8b2e57493 100644 --- a/investments/migrations/0002_initial.py +++ b/investments/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-12-05 23:27 +# Generated by Django 4.1.3 on 2022-12-06 18:44 from django.conf import settings from django.db import migrations, models @@ -10,8 +10,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("investments", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ diff --git a/investments/models.py b/investments/models.py index 53d0b142b..667331ade 100644 --- a/investments/models.py +++ b/investments/models.py @@ -1,20 +1,50 @@ from django.db import models from django.core.exceptions import ValidationError +from datetime import date import uuid def validate_amount(value): if value < 0 : raise ValidationError( - ('%(value)s is a invalid number, please only positive numbers!'), - params={"value": value} + (f'{value} is a invalid number, please only positive numbers!') ) +def validate_date(date_req): + today = date.today() + print(today) + + day = str(today).split("-")[2] + month = str(today).split("-")[1] + + if int(day) > 31: + raise ValidationError( + (f'{day} is a invalid day, please insert a valid day!') + ) + + if int(month) > 12: + raise ValidationError( + (f'{month} is a invalid month, please insert a valid month!') + ) + + today_formated = today.strftime("%d/%m/%Y") + + formated_date = date_req.strftime("%d/%m/%Y") + + if today_formated < formated_date: + raise ValidationError( + (f'{formated_date} is a invalid date, please investment only can be created in the past or today!') + ) + + class Investment(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False) + initial_amount = models.DecimalField(decimal_places=2,max_digits=10, null=True) amount = models.DecimalField(decimal_places=2,max_digits=10,validators=[validate_amount]) - created_at = models.DateField() + created_at = models.DateField(validators=[validate_date]) gains = models.DecimalField(decimal_places=2,max_digits=5, null=True, default=0) withdrawn_date = models.DateField(null=True) + expected_balance = models.DecimalField(decimal_places=2,max_digits=10, null=True, default=0) + isActive = models.BooleanField(default=True) owner = models.ForeignKey( "users.User", @@ -22,6 +52,5 @@ class Investment(models.Model): related_name="investments" ) -#Colocar gains, decimal e mudar amount -#Colocar withdrawnDate que começa vazio + #Caso tenha acontecido withdrawn colocar a data diff --git a/investments/serializers.py b/investments/serializers.py index 09d2a74e8..1e3cf9e8f 100644 --- a/investments/serializers.py +++ b/investments/serializers.py @@ -15,6 +15,8 @@ class Meta: "created_at", "gains", "owner_id", + "isActive", + "initial_amount", "withdrawn_date" ] @@ -22,6 +24,8 @@ class Meta: "id", "owner_id", "gains", + "isActive", + "initial_amount", "withdrawn_date" ] @@ -36,6 +40,9 @@ class Meta: "created_at", "gains", "owner", + "expected_balance", + "isActive", + "initial_amount", "withdrawn_date" ] @@ -43,5 +50,12 @@ class Meta: "id", "owner", "gains", + "isActive", + "expected_balance", + "initial_amount", "withdrawn_date" - ] \ No newline at end of file + ] + + def create(self, validated_data: dict) -> Investment: + validated_data['initial_amount'] = validated_data['amount'] + return Investment.objects.create(**validated_data) diff --git a/investments/views.py b/investments/views.py index c52cd5064..f8485bbba 100644 --- a/investments/views.py +++ b/investments/views.py @@ -1,16 +1,172 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics +from datetime import date +from decimal import Decimal import ipdb from investments.models import Investment from investments.serializers import InvestmentDetailSerializer, InvestmentSerializer from users.models import User +def validate_dates(investment, today_date, investment_date): + + today_separeted_date = str(today_date).split("/") + + separeted_date = str(investment_date).split("/") + + #Menos de 1 + if int(today_separeted_date[2]) == int(separeted_date[2]): + + if int(today_separeted_date[1]) > int(separeted_date[1]): + + months = int(today_separeted_date[1]) - int(separeted_date[1]) + + for _ in range(months): + new_gains = Decimal(investment.amount) * Decimal(0.52/100) + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + + investment.save() + + return investment + + #1 ano + + if int(today_separeted_date[2]) - int(separeted_date[2]) == 1 : + + if int(today_separeted_date[1]) == int(separeted_date[1]): + + if int(today_separeted_date[0]) == int(separeted_date[0]): + + new_gains = 12 * (Decimal(investment.amount) * Decimal((0.52/100))) + + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + + investment.save() + + return investment + + #1-2 anos + if ( + int(today_separeted_date[2]) - int(separeted_date[2]) == 1 + or + int(today_separeted_date[2]) - int(separeted_date[2]) == 2): + + years = int(today_separeted_date[2]) - int(separeted_date[2]) + + if int(today_separeted_date[1]) == int(separeted_date[1]): + + new_gains = (years * 12) * (Decimal(investment.amount) * Decimal((0.52/100))) + + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + + investment.save() + + return investment + + if int(today_separeted_date[1]) > int(separeted_date[1]): + months = int(today_separeted_date[1]) - int(separeted_date[1]) + + if int(today_separeted_date[0]) < int(separeted_date[0]): + months = int(separeted_date[1]) - int(today_separeted_date[1]) + + for _ in range(months - 1): + + new_gains = (Decimal(investment.amount) * Decimal((0.52/100))) + + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + + investment.save() + + return investment + years = int(today_separeted_date[2]) - int(separeted_date[2]) + + for _ in range(months): + + new_gains = (years * 12) * (Decimal(investment.amount) * Decimal((0.52/100))) + + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + + investment.save() + + return investment + + #+2anos + if int(today_separeted_date[2]) - int(separeted_date[2]) > 2: + + if int(today_separeted_date[1]) == int(separeted_date[1]): + + years = int(today_separeted_date[2]) - int(separeted_date[2]) + + new_gains = (years * 12) * (Decimal(investment.amount) * Decimal((0.52/100))) + + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + + investment.save() + + return investment + + if int(today_separeted_date[1]) > int(separeted_date[1]): + months = int(today_separeted_date[1]) - int(separeted_date[1]) + years = int(today_separeted_date[2]) - int(separeted_date[2]) + + if int(today_separeted_date[0]) < int(separeted_date[0]): + + for _ in range(months - 1): + + new_gains = (years * 12) * (Decimal(investment.amount) * Decimal((0.52/100))) + + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + + investment.save() + + return investment + + for _ in range(months): + + new_gains = (years * 12) * (Decimal(investment.amount) * Decimal((0.52/100))) + + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + + investment.save() + + return investment + class ListInvestmentView(generics.ListAPIView): queryset = Investment.objects.all() serializer_class = InvestmentSerializer - class CreateInvestmentView(generics.CreateAPIView): queryset = Investment.objects.all() @@ -34,6 +190,22 @@ class RetrieveUpdateDestroyInvestmentDetailView( lookup_url_kwarg = 'investment_id' + def retrieve(self, request, *args, **kwargs): + investment_id = self.kwargs['investment_id'] + + investment = get_object_or_404(Investment, pk=investment_id) + + today_date = date.today() + + today_date_formated = today_date.strftime("%d/%m/%Y") + + formated_date = investment.created_at.strftime("%d/%m/%Y") + + result = validate_dates(investment, today_date_formated, formated_date) + + return super().retrieve(result) + + def destroy(self, request, *args, **kwargs): ... diff --git a/investments_management/settings.py b/investments_management/settings.py index 6b5118925..0223505c4 100644 --- a/investments_management/settings.py +++ b/investments_management/settings.py @@ -142,5 +142,5 @@ REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", - "PAGE_SIZE": 5, + "PAGE_SIZE": 10, } \ No newline at end of file diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index f2c745529..0cfb9b763 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-12-05 23:27 +# Generated by Django 4.1.3 on 2022-12-06 18:44 import django.contrib.auth.models import django.contrib.auth.validators From ff4edc171fd06b554cd77fab4efe4ca45e687d33 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Tue, 6 Dec 2022 18:38:11 -0400 Subject: [PATCH 08/12] feat: created withdrawn with taxes --- investments/migrations/0001_initial.py | 18 ++- investments/migrations/0002_initial.py | 2 +- investments/models.py | 42 ++++++- investments/serializers.py | 164 ++++++++++++++++++++++++- investments/urls.py | 6 +- investments/views.py | 47 ++++--- investments_management/settings.py | 1 + users/migrations/0001_initial.py | 2 +- utilities/api_exceptions.py | 27 ++++ 9 files changed, 275 insertions(+), 34 deletions(-) create mode 100644 utilities/api_exceptions.py diff --git a/investments/migrations/0001_initial.py b/investments/migrations/0001_initial.py index 5cfa639cc..2217a933f 100644 --- a/investments/migrations/0001_initial.py +++ b/investments/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-12-06 18:44 +# Generated by Django 4.1.3 on 2022-12-06 21:57 from django.db import migrations, models import investments.models @@ -26,7 +26,7 @@ class Migration(migrations.Migration): ), ( "initial_amount", - models.DecimalField(decimal_places=2, max_digits=10, null=True), + models.DecimalField(decimal_places=2, max_digits=15, null=True), ), ( "amount", @@ -46,11 +46,21 @@ class Migration(migrations.Migration): decimal_places=2, default=0, max_digits=5, null=True ), ), - ("withdrawn_date", models.DateField(null=True)), + ( + "withdrawn_date", + models.DateField( + null=True, + validators=[investments.models.validate_withdrawn_date], + ), + ), + ( + "withdrew_amount", + models.DecimalField(decimal_places=2, max_digits=15, null=True), + ), ( "expected_balance", models.DecimalField( - decimal_places=2, default=0, max_digits=10, null=True + decimal_places=2, default=0, max_digits=15, null=True ), ), ("isActive", models.BooleanField(default=True)), diff --git a/investments/migrations/0002_initial.py b/investments/migrations/0002_initial.py index 8b2e57493..3037c3834 100644 --- a/investments/migrations/0002_initial.py +++ b/investments/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-12-06 18:44 +# Generated by Django 4.1.3 on 2022-12-06 21:57 from django.conf import settings from django.db import migrations, models diff --git a/investments/models.py b/investments/models.py index 667331ade..3cb5343e3 100644 --- a/investments/models.py +++ b/investments/models.py @@ -1,8 +1,11 @@ from django.db import models from django.core.exceptions import ValidationError from datetime import date +import ipdb import uuid +from utilities.api_exceptions import CustomApiException + def validate_amount(value): if value < 0 : raise ValidationError( @@ -11,7 +14,6 @@ def validate_amount(value): def validate_date(date_req): today = date.today() - print(today) day = str(today).split("-")[2] month = str(today).split("-")[1] @@ -34,16 +36,46 @@ def validate_date(date_req): raise ValidationError( (f'{formated_date} is a invalid date, please investment only can be created in the past or today!') ) - + +def validate_withdrawn_date(date_req): + today = date.today() + + if int(str(today).split("-")[2]) > 31: + ipdb.set_trace() + raise CustomApiException(400, "Invalid Date!") + + if int(str(today).split("-")[1]) > 12: + ipdb.set_trace() + + raise CustomApiException(400, "Invalid Date!") + + today_formated = str(today) + + formated_date = str(date_req) + + if today_formated.split('-')[0] < formated_date.split('-')[0]: + raise CustomApiException(400, "Invalid Date!") + + if today_formated.split('-')[0] == formated_date.split('-')[0]: + if today_formated.split('-')[1] < formated_date.split('-')[1]: + raise CustomApiException(400, "Invalid Date!") + + + if today_formated.split('-')[0] == formated_date.split('-')[0]: + if today_formated.split('-')[1] == formated_date.split('-')[1]: + if today_formated.split('-')[2] < formated_date.split('-')[2]: + raise CustomApiException(400, "Invalid Date!") + class Investment(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False) - initial_amount = models.DecimalField(decimal_places=2,max_digits=10, null=True) + initial_amount = models.DecimalField(decimal_places=2,max_digits=15, null=True) amount = models.DecimalField(decimal_places=2,max_digits=10,validators=[validate_amount]) created_at = models.DateField(validators=[validate_date]) gains = models.DecimalField(decimal_places=2,max_digits=5, null=True, default=0) - withdrawn_date = models.DateField(null=True) - expected_balance = models.DecimalField(decimal_places=2,max_digits=10, null=True, default=0) + withdrawn_date = models.DateField(null=True, validators=[validate_withdrawn_date]) + withdrew_amount = models.DecimalField(decimal_places=2,max_digits=15, null=True) + expected_balance = models.DecimalField(decimal_places=2,max_digits=15, null=True, default=0) isActive = models.BooleanField(default=True) owner = models.ForeignKey( diff --git a/investments/serializers.py b/investments/serializers.py index 1e3cf9e8f..5c32a3e45 100644 --- a/investments/serializers.py +++ b/investments/serializers.py @@ -1,10 +1,113 @@ +from decimal import Decimal from django.shortcuts import get_object_or_404 from rest_framework import serializers +from django.core.exceptions import ValidationError from investments.models import Investment from users.models import User import ipdb +from utilities.api_exceptions import CustomApiException + +def validate_dates_and_tax(investment, today_date, investment_date): + + today_separeted_date = str(today_date).split("/") + + separeted_date = str(investment_date).split("/") + + if int(today_separeted_date[2]) == int(separeted_date[2]): + if int(today_separeted_date[1]) > int(separeted_date[1]): + + result = investment.amount - investment.initial_amount + + tax = result * Decimal(22.5/100) + + charged = investment.amount - Decimal(tax) + + return round(charged, 2) + + if int(today_separeted_date[2]) - int(separeted_date[2]) == 1 : + + if int(today_separeted_date[1]) == int(separeted_date[1]): + + if int(today_separeted_date[0]) == int(separeted_date[0]): + + result = investment.amount - investment.initial_amount + + tax = result * Decimal(18.5/100) + + charged = investment.amount - Decimal(tax) + + return round(charged, 2) + + if ( + int(today_separeted_date[2]) - int(separeted_date[2]) == 1 + or + int(today_separeted_date[2]) - int(separeted_date[2]) == 2): + + if int(today_separeted_date[1]) == int(separeted_date[1]): + if int(today_separeted_date[0]) == int(separeted_date[0]): + result = investment.amount - investment.initial_amount + + tax = result * Decimal(18.5/100) + + charged = investment.amount - Decimal(tax) + + return round(charged, 2) + + + if int(today_separeted_date[1]) > int(separeted_date[1]): + + if int(today_separeted_date[0]) < int(separeted_date[0]): + + result = investment.amount - investment.initial_amount + + tax = result * Decimal(18.5/100) + + charged = investment.amount - Decimal(tax) + + return round(charged, 2) + + result = investment.amount - investment.initial_amount + + tax = result * Decimal(18.5/100) + + charged = investment.amount - Decimal(tax) + + return round(charged, 2) + + if int(today_separeted_date[2]) - int(separeted_date[2]) > 2: + + if int(today_separeted_date[1]) == int(separeted_date[1]): + + result = investment.amount - investment.initial_amount + + tax = result * Decimal(18.5/100) + + charged = investment.amount - Decimal(tax) + + return round(charged, 2) + + if int(today_separeted_date[1]) > int(separeted_date[1]): + if int(today_separeted_date[0]) < int(separeted_date[0]): + + result = investment.amount - investment.initial_amount + + tax = result * Decimal(18.5/100) + + charged = investment.amount - Decimal(tax) + + return round(charged, 2) + + result = investment.amount - investment.initial_amount + + tax = result * Decimal(18.5/100) + + charged = investment.amount - Decimal(tax) + + return round(charged, 2) + + class InvestmentSerializer(serializers.ModelSerializer): class Meta: model = Investment @@ -41,21 +144,74 @@ class Meta: "gains", "owner", "expected_balance", - "isActive", "initial_amount", - "withdrawn_date" ] read_only_fields=[ "id", "owner", "gains", - "isActive", "expected_balance", "initial_amount", - "withdrawn_date" ] def create(self, validated_data: dict) -> Investment: validated_data['initial_amount'] = validated_data['amount'] return Investment.objects.create(**validated_data) + +class InvestmentWithdrawnDetailSerializer(serializers.ModelSerializer): + + class Meta: + model = Investment + + fields = [ + "id", + "amount", + "created_at", + "gains", + "owner", + "expected_balance", + "initial_amount", + "withdrawn_date", + "withdrew_amount" + ] + + read_only_fields=[ + "id", + "amount", + "created_at", + "gains", + "owner", + "expected_balance", + "isActive", + "initial_amount", + "withdrew_amount" + ] + + def create(self, validated_data: dict) -> Investment: + investment_id = self.context['view'].kwargs['investment_id'] + + investment = get_object_or_404(Investment, pk=investment_id) + # ipdb.set_trace() + if investment.withdrawn_date != None: + raise CustomApiException(409, "Investment has already been withdrew!") + + date = validated_data["withdrawn_date"] + + today_date = date.today() + + today_date_formated = today_date.strftime("%d/%m/%Y") + + formated_date = investment.created_at.strftime("%d/%m/%Y") + + result = validate_dates_and_tax(investment, today_date_formated, formated_date) + + investment.withdrew_amount = result + investment.withdrawn_date = date + investment.amount = 0 + investment.expected_balance = 0 + investment.gains = 0 + + investment.save() + + return investment \ No newline at end of file diff --git a/investments/urls.py b/investments/urls.py index 73c57269e..c1790b4b2 100644 --- a/investments/urls.py +++ b/investments/urls.py @@ -1,10 +1,12 @@ from django.urls import path -from investments.views import CreateInvestmentView, ListInvestmentView, RetrieveUpdateDestroyInvestmentDetailView +from investments.views import CreateInvestmentView, ListInvestmentView, RetrieveUpdateDestroyInvestmentDetailView, WithdrawnInvestmentView urlpatterns=[ path('investments/', ListInvestmentView.as_view()), path('investments//', CreateInvestmentView.as_view()), - path('investment//', RetrieveUpdateDestroyInvestmentDetailView.as_view()) + path('investment//', RetrieveUpdateDestroyInvestmentDetailView.as_view()), + path('investment//withdrawn/', WithdrawnInvestmentView.as_view()), + ] diff --git a/investments/views.py b/investments/views.py index f8485bbba..b17c91e47 100644 --- a/investments/views.py +++ b/investments/views.py @@ -3,9 +3,10 @@ from datetime import date from decimal import Decimal import ipdb +from uritemplate import partial from investments.models import Investment -from investments.serializers import InvestmentDetailSerializer, InvestmentSerializer +from investments.serializers import InvestmentDetailSerializer, InvestmentSerializer, InvestmentWithdrawnDetailSerializer from users.models import User def validate_dates(investment, today_date, investment_date): @@ -16,7 +17,6 @@ def validate_dates(investment, today_date, investment_date): #Menos de 1 if int(today_separeted_date[2]) == int(separeted_date[2]): - if int(today_separeted_date[1]) > int(separeted_date[1]): months = int(today_separeted_date[1]) - int(separeted_date[1]) @@ -34,7 +34,6 @@ def validate_dates(investment, today_date, investment_date): return investment #1 ano - if int(today_separeted_date[2]) - int(separeted_date[2]) == 1 : if int(today_separeted_date[1]) == int(separeted_date[1]): @@ -62,23 +61,28 @@ def validate_dates(investment, today_date, investment_date): years = int(today_separeted_date[2]) - int(separeted_date[2]) if int(today_separeted_date[1]) == int(separeted_date[1]): + if int(today_separeted_date[0]) == int(separeted_date[0]): + print("1-2 1") + new_gains = (years * 12) * (Decimal(investment.amount) * Decimal((0.52/100))) - new_gains = (years * 12) * (Decimal(investment.amount) * Decimal((0.52/100))) - - expected_balance = (new_gains + Decimal(investment.amount)) - - investment.gains = round(new_gains,2) - investment.amount = expected_balance - investment.expected_balance = round(expected_balance + new_gains , 2) + expected_balance = (new_gains + Decimal(investment.amount)) + + investment.gains = round(new_gains,2) + investment.amount = expected_balance + investment.expected_balance = round(expected_balance + new_gains , 2) + # ipdb.set_trace() - investment.save() - - return investment + investment.save() + + return investment + if int(today_separeted_date[1]) > int(separeted_date[1]): + months = int(today_separeted_date[1]) - int(separeted_date[1]) if int(today_separeted_date[0]) < int(separeted_date[0]): + months = int(separeted_date[1]) - int(today_separeted_date[1]) for _ in range(months - 1): @@ -90,10 +94,10 @@ def validate_dates(investment, today_date, investment_date): investment.gains = round(new_gains,2) investment.amount = expected_balance investment.expected_balance = round(expected_balance + new_gains , 2) - investment.save() return investment + years = int(today_separeted_date[2]) - int(separeted_date[2]) for _ in range(months): @@ -112,7 +116,7 @@ def validate_dates(investment, today_date, investment_date): #+2anos if int(today_separeted_date[2]) - int(separeted_date[2]) > 2: - + if int(today_separeted_date[1]) == int(separeted_date[1]): years = int(today_separeted_date[2]) - int(separeted_date[2]) @@ -182,7 +186,7 @@ def perform_create(self, serializer): serializer.save(owner=owner) class RetrieveUpdateDestroyInvestmentDetailView( - generics.RetrieveUpdateDestroyAPIView + generics.RetrieveDestroyAPIView ): queryset = Investment.objects.all() @@ -209,4 +213,13 @@ def retrieve(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): ... -# mudar o delete para soft delete \ No newline at end of file +# mudar o delete para soft delete + +class WithdrawnInvestmentView(generics.CreateAPIView): + queryset = Investment.objects.all() + serializer_class = InvestmentWithdrawnDetailSerializer + + lookup_url_kwarg = 'investment_id' + + + \ No newline at end of file diff --git a/investments_management/settings.py b/investments_management/settings.py index 0223505c4..e4bbd207c 100644 --- a/investments_management/settings.py +++ b/investments_management/settings.py @@ -143,4 +143,5 @@ REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, + 'EXCEPTION_HANDLER': 'utilities.api_exceptions.custom_exception_handler', } \ No newline at end of file diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index 0cfb9b763..583211bca 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.3 on 2022-12-06 18:44 +# Generated by Django 4.1.3 on 2022-12-06 21:57 import django.contrib.auth.models import django.contrib.auth.validators diff --git a/utilities/api_exceptions.py b/utilities/api_exceptions.py new file mode 100644 index 000000000..232ba4720 --- /dev/null +++ b/utilities/api_exceptions.py @@ -0,0 +1,27 @@ +from rest_framework.views import exception_handler +from rest_framework.exceptions import APIException + +def custom_exception_handler(exc, context): + + response = exception_handler(exc, context) + + if response is not None: + response.data['status_code'] = response.status_code + + #replace detail key with message key by delete detail key + response.data['message'] = response.data['detail'] + del response.data['detail'] + + return response + +class CustomApiException(APIException): + + #public fields + detail = None + status_code = None + + # create constructor + def __init__(self, status_code, message): + #override public fields + CustomApiException.status_code = status_code + CustomApiException.detail = message \ No newline at end of file From d90a6eba74c1d94ea5ffb3619067a014286a1ad5 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Tue, 6 Dec 2022 19:02:57 -0400 Subject: [PATCH 09/12] feat: basic read.me, created doc route and create postgreSQL connection --- README.md | 128 +++---- investments_management/settings.py | 19 +- requirements.txt | 17 + schema.yml | 543 +++++++++++++++++++++++++++++ users/urls.py | 4 +- 5 files changed, 620 insertions(+), 91 deletions(-) create mode 100644 schema.yml diff --git a/README.md b/README.md index ea8115e67..a243e4b88 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,42 @@ -# Back End Test Project +# Tenha controle de seus investimentos + +##### Projeto no qual você pode guardar seus investimentos ou de outras pessoas e vê-los crescendo com a quantia que colocou com os juros compostos, você também pode retirá-los, o que ocasionará no retiro total da quantia e tendo ainda um histórico de investimentos em cada conta! + +## Tecnologias Utilizadas + +#### Neste projeto foram usados as seguintes tecnologias: + +``` + - Python + - Django Rest Framework + - PostgreSQL + - UUID + - SQLite3 + - Ipdb +``` + +#### Assim que clonar o repositório entre na pasta com : + - cd backend-test-nomedousuário + - code . + +## Abra o terminal para instalar as dependências no ambiente virtual(venv): + #### Para utilizar o venv se estiver utilizando Linux use o comando: + + source venv/bin/activate + #### Para utilizar o venv se estiver utilizando Windows use o comando: + + source venv/Scripts/activate + + ### Para instalar as dependências use o comando: + pip install -r requirements.txt + ##### Dentro do arquivo requirements.txt está todos as denpendências que o projeto necessita para o código rodar normalmente. + +## Após instalar pode se iniciar o projeto com + ### Rodando as migrations com + python manage.py migrate + ### Após rodar as migrations pode iniciar o projeto com: + python manage.py runserver + ### Para acessar a documentação, após iniciar o servidor, possui a rota +## /api/docs +### Nesta rota acessando pelo localhost:****/api/docs terá acesso a documentação da api! -You should see this challenge as an opportunity to create an application following modern development best practices (given the stack of your choice), but also feel free to use your own architecture preferences (coding standards, code organization, third-party libraries, etc). It’s perfectly fine to use vanilla code or any framework or libraries. - -## Scope - -In this challenge you should build an API for an application that stores and manages investments, it should have the following features: - -1. __Creation__ of an investment with an owner, a creation date and an amount. - 1. The creation date of an investment can be today or a date in the past. - 2. An investment should not be or become negative. -2. __View__ of an investment with its initial amount and expected balance. - 1. Expected balance should be the sum of the invested amount and the [gains][]. - 2. If an investment was already withdrawn then the balance must reflect the gains of that investment -3. __Withdrawal__ of a investment. - 1. The withdraw will always be the sum of the initial amount and its gains, - partial withdrawn is not supported. - 2. Withdrawals can happen in the past or today, but can't happen before the investment creation or the future. - 3. [Taxes][taxes] need to be applied to the withdrawals before showing the final value. -4. __List__ of a person's investments - 1. This list should have pagination. - -__NOTE:__ the implementation of an interface will not be evaluated. - -### Gain Calculation - -The investment will pay 0.52% every month in the same day of the investment creation. - -Given that the gain is paid every month, it should be treated as [compound gain][], which means that every new period (month) the amount gained will become part of the investment balance for the next payment. - -### Taxation - -When money is withdrawn, tax is triggered. Taxes apply only to the profit/gain portion of the money withdrawn. For example, if the initial investment was 1000.00, the current balance is 1200.00, then the taxes will be applied to the 200.00. - -The tax percentage changes according to the age of the investment: -* If it is less than one year old, the percentage will be 22.5% (tax = 45.00). -* If it is between one and two years old, the percentage will be 18.5% (tax = 37.00). -* If older than two years, the percentage will be 15% (tax = 30.00). - -## Requirements -1. Create project using any technology of your preference. It’s perfectly OK to use vanilla code or any framework or libraries; -2. Although you can use as many dependencies as you want, you should manage them wisely; -3. It is not necessary to send the notification emails, however, the code required for that would be welcome; -4. The API must be documented in some way. - -## Deliverables -The project source code and dependencies should be made available in GitHub. Here are the steps you should follow: -1. Fork this repository to your GitHub account (create an account if you don't have one, you will need it working with us). -2. Create a "development" branch and commit the code to it. Do not push the code to the main branch. -3. Include a README file that describes: - - Special build instructions, if any - - List of third-party libraries used and short description of why/how they were used - - A link to the API documentation. -4. Once the work is complete, create a pull request from "development" into "main" and send us the link. -5. Avoid using huge commits hiding your progress. Feel free to work on a branch and use `git rebase` to adjust your commits before submitting the final version. - -## Coding Standards -When working on the project be as clean and consistent as possible. - -## Project Deadline -Ideally you'd finish the test project in 5 days. It shouldn't take you longer than a entire week. - -## Quality Assurance -Use the following checklist to ensure high quality of the project. - -### General -- First of all, the application should run without errors. -- Are all requirements set above met? -- Is coding style consistent? -- The API is well documented? -- The API has unit tests? - -## Submission -1. A link to the Github repository. -2. Briefly describe how you decided on the tools that you used. - -## Have Fun Coding 🤘 -- This challenge description is intentionally vague in some aspects, but if you need assistance feel free to ask for help. -- If any of the seems out of your current level, you may skip it, but remember to tell us about it in the pull request. - -## Credits - -This coding challenge was inspired on [kinvoapp/kinvo-back-end-test](https://github.com/kinvoapp/kinvo-back-end-test/blob/2f17d713de739e309d17a1a74a82c3fd0e66d128/README.md) - -[gains]: #gain-calculation -[taxes]: #taxation -[interest]: #interest-calculation -[compound gain]: https://www.investopedia.com/terms/g/gain.asp diff --git a/investments_management/settings.py b/investments_management/settings.py index e4bbd207c..da6a2829d 100644 --- a/investments_management/settings.py +++ b/investments_management/settings.py @@ -91,9 +91,13 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("POSTGRES_DB"), + "USER": os.getenv("POSTGRES_USER"), + "PASSWORD": os.getenv("POSTGRES_PASSWORD"), + "HOST": "127.0.0.1", + "PORT": 5432, + } } @@ -144,4 +148,13 @@ "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, 'EXCEPTION_HANDLER': 'utilities.api_exceptions.custom_exception_handler', + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + 'TITLE': 'Test Back End - Coderockr', + 'DESCRIPTION': 'Coderockr Backend recruitment test ', + 'VERSION': '1.0.0', + 'SERVE_INCLUDE_SCHEMA': False, + # OTHER SETTINGS } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9ef78ab10..af06039c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,42 @@ asgiref==3.5.2 asttokens==2.2.0 +attrs==22.1.0 backcall==0.2.0 +black==22.10.0 +click==8.1.3 colorama==0.4.6 +coverage==6.5.0 decorator==5.1.1 Django==4.1.3 djangorestframework==3.14.0 +drf-spectacular==0.24.2 executing==1.2.0 +inflection==0.5.1 ipdb==0.13.9 ipython==8.7.0 jedi==0.18.2 +jsonschema==4.17.3 matplotlib-inline==0.1.6 +mypy-extensions==0.4.3 parso==0.8.3 +pathspec==0.10.2 pickleshare==0.7.5 +platformdirs==2.5.4 prompt-toolkit==3.0.33 +psycopg2-binary==2.9.5 pure-eval==0.2.2 +pycodestyle==2.10.0 Pygments==2.13.0 +pyrsistent==0.19.2 +python-dotenv==0.21.0 pytz==2022.6 +PyYAML==6.0 six==1.16.0 sqlparse==0.4.3 stack-data==0.6.2 toml==0.10.2 +tomli==2.0.1 traitlets==5.6.0 tzdata==2022.7 +uritemplate==4.1.1 wcwidth==0.2.5 diff --git a/schema.yml b/schema.yml new file mode 100644 index 000000000..cce3a3261 --- /dev/null +++ b/schema.yml @@ -0,0 +1,543 @@ +openapi: 3.0.3 +info: + title: Test Back End - Coderockr + version: 1.0.0 + description: 'Coderockr Backend recruitment test ' +paths: + /api/accounts/: + get: + operationId: api_accounts_list + parameters: + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + tags: + - api + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedUserList' + description: '' + post: + operationId: api_accounts_create + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + multipart/form-data: + schema: + $ref: '#/components/schemas/User' + required: true + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: '' + /api/accounts/{id}/: + get: + operationId: api_accounts_retrieve + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - api + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetail' + description: '' + put: + operationId: api_accounts_update + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UserDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/UserDetail' + required: true + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetail' + description: '' + patch: + operationId: api_accounts_partial_update + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedUserDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedUserDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedUserDetail' + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetail' + description: '' + delete: + operationId: api_accounts_destroy + parameters: + - in: path + name: id + schema: + type: string + required: true + tags: + - api + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '204': + description: No response body + /api/investment/{investment_id}/: + get: + operationId: api_investment_retrieve + parameters: + - in: path + name: investment_id + schema: + type: string + required: true + tags: + - api + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/InvestmentDetail' + description: '' + delete: + operationId: api_investment_destroy + parameters: + - in: path + name: investment_id + schema: + type: string + required: true + tags: + - api + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '204': + description: No response body + /api/investment/{investment_id}/withdrawn/: + post: + operationId: api_investment_withdrawn_create + parameters: + - in: path + name: investment_id + schema: + type: string + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InvestmentWithdrawnDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/InvestmentWithdrawnDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/InvestmentWithdrawnDetail' + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/InvestmentWithdrawnDetail' + description: '' + /api/investments/: + get: + operationId: api_investments_list + parameters: + - name: page + required: false + in: query + description: A page number within the paginated result set. + schema: + type: integer + tags: + - api + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedInvestmentList' + description: '' + /api/investments/{owner_id}/: + post: + operationId: api_investments_create + parameters: + - in: path + name: owner_id + schema: + type: string + required: true + tags: + - api + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InvestmentDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/InvestmentDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/InvestmentDetail' + required: true + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/InvestmentDetail' + description: '' +components: + schemas: + Investment: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + amount: + type: string + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ + created_at: + type: string + format: date + gains: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + owner_id: + type: string + format: uuid + readOnly: true + isActive: + type: boolean + readOnly: true + initial_amount: + type: string + format: decimal + pattern: ^-?\d{0,13}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + withdrawn_date: + type: string + format: date + readOnly: true + nullable: true + required: + - amount + - created_at + - gains + - id + - initial_amount + - isActive + - owner_id + - withdrawn_date + InvestmentDetail: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + amount: + type: string + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ + created_at: + type: string + format: date + gains: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + owner: + type: string + format: uuid + readOnly: true + expected_balance: + type: string + format: decimal + pattern: ^-?\d{0,13}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + initial_amount: + type: string + format: decimal + pattern: ^-?\d{0,13}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + required: + - amount + - created_at + - expected_balance + - gains + - id + - initial_amount + - owner + InvestmentWithdrawnDetail: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + amount: + type: string + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ + readOnly: true + created_at: + type: string + format: date + readOnly: true + gains: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + owner: + type: string + format: uuid + readOnly: true + expected_balance: + type: string + format: decimal + pattern: ^-?\d{0,13}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + initial_amount: + type: string + format: decimal + pattern: ^-?\d{0,13}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + withdrawn_date: + type: string + format: date + nullable: true + withdrew_amount: + type: string + format: decimal + pattern: ^-?\d{0,13}(?:\.\d{0,2})?$ + readOnly: true + nullable: true + required: + - amount + - created_at + - expected_balance + - gains + - id + - initial_amount + - owner + - withdrew_amount + PaginatedInvestmentList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/Investment' + PaginatedUserList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/User' + PatchedUserDetail: + type: object + properties: + username: + type: string + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + pattern: ^[\w.@+-]+$ + maxLength: 150 + email: + type: string + maxLength: 127 + investments: + type: array + items: + $ref: '#/components/schemas/Investment' + User: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + email: + type: string + maxLength: 127 + username: + type: string + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + pattern: ^[\w.@+-]+$ + maxLength: 150 + required: + - email + - id + - username + UserDetail: + type: object + properties: + username: + type: string + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + pattern: ^[\w.@+-]+$ + maxLength: 150 + email: + type: string + maxLength: 127 + investments: + type: array + items: + $ref: '#/components/schemas/Investment' + required: + - email + - investments + - username + securitySchemes: + basicAuth: + type: http + scheme: basic + cookieAuth: + type: apiKey + in: cookie + name: sessionid diff --git a/users/urls.py b/users/urls.py index c636e847a..6b407ec51 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,4 +1,5 @@ from django.urls import path +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView @@ -8,5 +9,6 @@ path('accounts/', CreateUserView.as_view()), path('accounts//', ListUpdateDeleteDetailUserView.as_view()), path('schema/', SpectacularAPIView.as_view(), name='schema'), - path('docs/', SpectacularRedocView.as_view(url_name='schema'), name='redoc') + path('docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), + path('schema/', SpectacularAPIView.as_view(), name='schema'), ] From 9809b3886961a116ede873019c5f5ad1c9596464 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Wed, 7 Dec 2022 10:29:25 -0400 Subject: [PATCH 10/12] feat: create test models and views --- .gitignore | 2 +- investments/baker_recipes.py | 8 ++ investments/models.py | 39 +++++--- investments/serializers.py | 28 ++++-- investments/tests.py | 3 - investments/tests/__init__.py | 0 investments/tests/test_models.py | 30 +++++++ investments/tests/test_views.py | 119 +++++++++++++++++++++++++ investments/views.py | 31 ++++--- investments_management/settings.py | 1 - requirements.txt | 1 + users/baker_recipes.py | 8 ++ users/migrations/0002_user_isactive.py | 18 ++++ users/models.py | 2 + users/serializers.py | 24 +++-- users/tests.py | 3 - users/tests/__init__.py | 0 users/tests/test_models.py | 22 +++++ users/tests/test_views.py | 52 +++++++++++ users/views.py | 14 +++ utilities/api_exceptions.py | 27 ------ 21 files changed, 355 insertions(+), 77 deletions(-) create mode 100644 investments/baker_recipes.py delete mode 100644 investments/tests.py create mode 100644 investments/tests/__init__.py create mode 100644 investments/tests/test_models.py create mode 100644 investments/tests/test_views.py create mode 100644 users/baker_recipes.py create mode 100644 users/migrations/0002_user_isactive.py delete mode 100644 users/tests.py create mode 100644 users/tests/__init__.py create mode 100644 users/tests/test_models.py create mode 100644 users/tests/test_views.py delete mode 100644 utilities/api_exceptions.py diff --git a/.gitignore b/.gitignore index 9b3f0c40d..c8561f5cf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ __pycache__ .env db.sqlite3 .vscode -.coverage + diff --git a/investments/baker_recipes.py b/investments/baker_recipes.py new file mode 100644 index 000000000..0670c591f --- /dev/null +++ b/investments/baker_recipes.py @@ -0,0 +1,8 @@ +from model_bakery.recipe import Recipe +from investments.models import Investment + +new_investment = Recipe( + Investment, + amount= 100, + created_at= "2022-10-06" +) \ No newline at end of file diff --git a/investments/models.py b/investments/models.py index 3cb5343e3..cdc7a11e1 100644 --- a/investments/models.py +++ b/investments/models.py @@ -3,8 +3,17 @@ from datetime import date import ipdb import uuid +from rest_framework.exceptions import APIException +from rest_framework.views import status -from utilities.api_exceptions import CustomApiException +class CustomValidation(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "A server error occured." + + def __init__(self, detail, status_code): + if status_code is not None:self.status_code=status_code + if detail is not None: + self.detail={"detail": detail} def validate_amount(value): if value < 0 : @@ -32,39 +41,47 @@ def validate_date(date_req): formated_date = date_req.strftime("%d/%m/%Y") - if today_formated < formated_date: - raise ValidationError( - (f'{formated_date} is a invalid date, please investment only can be created in the past or today!') - ) + if today_formated.split('/')[2] < formated_date.split('/')[2]: + raise CustomValidation("Invalid Date!", 400) + + if today_formated.split('/')[2] == formated_date.split('/')[2]: + if today_formated.split('/')[1] < formated_date.split('/')[1]: + raise CustomValidation("Invalid Date!", 400) + + + if today_formated.split('/')[2] == formated_date.split('/')[2]: + if today_formated.split('/')[1] == formated_date.split('/')[1]: + if today_formated.split('/')[0] < formated_date.split('/')[0]: + raise CustomValidation("Invalid Date!", 400) def validate_withdrawn_date(date_req): today = date.today() if int(str(today).split("-")[2]) > 31: ipdb.set_trace() - raise CustomApiException(400, "Invalid Date!") + raise CustomValidation("Invalid Date!",400) if int(str(today).split("-")[1]) > 12: ipdb.set_trace() - raise CustomApiException(400, "Invalid Date!") + raise CustomValidation("Invalid Date!",400) today_formated = str(today) formated_date = str(date_req) if today_formated.split('-')[0] < formated_date.split('-')[0]: - raise CustomApiException(400, "Invalid Date!") + raise CustomValidation("Invalid Date!",400) if today_formated.split('-')[0] == formated_date.split('-')[0]: if today_formated.split('-')[1] < formated_date.split('-')[1]: - raise CustomApiException(400, "Invalid Date!") + raise CustomValidation("Invalid Date!",400) if today_formated.split('-')[0] == formated_date.split('-')[0]: if today_formated.split('-')[1] == formated_date.split('-')[1]: if today_formated.split('-')[2] < formated_date.split('-')[2]: - raise CustomApiException(400, "Invalid Date!") + raise CustomValidation("Invalid Date!",400) class Investment(models.Model): @@ -84,5 +101,3 @@ class Investment(models.Model): related_name="investments" ) - -#Caso tenha acontecido withdrawn colocar a data diff --git a/investments/serializers.py b/investments/serializers.py index 5c32a3e45..cc0830b15 100644 --- a/investments/serializers.py +++ b/investments/serializers.py @@ -1,14 +1,23 @@ from decimal import Decimal from django.shortcuts import get_object_or_404 from rest_framework import serializers -from django.core.exceptions import ValidationError from investments.models import Investment -from users.models import User import ipdb +from rest_framework.exceptions import APIException +from rest_framework.views import status + +from users.models import User -from utilities.api_exceptions import CustomApiException +class CustomValidation(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "A server error occured." + def __init__(self, detail, status_code): + if status_code is not None:self.status_code=status_code + if detail is not None: + self.detail={"detail": detail} + def validate_dates_and_tax(investment, today_date, investment_date): today_separeted_date = str(today_date).split("/") @@ -107,7 +116,6 @@ def validate_dates_and_tax(investment, today_date, investment_date): return round(charged, 2) - class InvestmentSerializer(serializers.ModelSerializer): class Meta: model = Investment @@ -156,8 +164,13 @@ class Meta: ] def create(self, validated_data: dict) -> Investment: + owner_id = self.context['view'].kwargs['owner_id'] + + owner = get_object_or_404(User, pk=owner_id) + validated_data['initial_amount'] = validated_data['amount'] - return Investment.objects.create(**validated_data) + + return Investment.objects.create(**validated_data, owner=owner) class InvestmentWithdrawnDetailSerializer(serializers.ModelSerializer): @@ -192,9 +205,9 @@ def create(self, validated_data: dict) -> Investment: investment_id = self.context['view'].kwargs['investment_id'] investment = get_object_or_404(Investment, pk=investment_id) - # ipdb.set_trace() + if investment.withdrawn_date != None: - raise CustomApiException(409, "Investment has already been withdrew!") + raise CustomValidation(409, "Investment has already been withdrew!") date = validated_data["withdrawn_date"] @@ -206,6 +219,7 @@ def create(self, validated_data: dict) -> Investment: result = validate_dates_and_tax(investment, today_date_formated, formated_date) + investment.initial_amount = investment.amount investment.withdrew_amount = result investment.withdrawn_date = date investment.amount = 0 diff --git a/investments/tests.py b/investments/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/investments/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/investments/tests/__init__.py b/investments/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/investments/tests/test_models.py b/investments/tests/test_models.py new file mode 100644 index 000000000..3b85acb7b --- /dev/null +++ b/investments/tests/test_models.py @@ -0,0 +1,30 @@ +from django.test import TestCase +from model_bakery import baker +import ipdb + +class InvestmentModelTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.investment_create = baker.make_recipe('investments.new_investment') + + def test_name_max_legth(self): + + max_length = self.investment_create._meta.get_field('amount').max_digits + + self.assertEqual(max_length, 10) + + def test_has_correct_fields(self): + self.assertEqual(self.investment_create.amount, 100) + self.assertEqual(self.investment_create.created_at, "2022-10-06") + self.assertEqual(self.investment_create.gains, 0) + self.assertEqual(self.investment_create.withdrawn_date, None) + self.assertEqual(self.investment_create.expected_balance, 0) + self.assertEqual(self.investment_create.initial_amount, None) + self.assertTrue(self.investment_create.isActive) + self.assertTrue(self.investment_create.owner_id) + + def test_amount_number_is_positive(self): + self.assertTrue(self.investment_create.amount > 0) + + def test_episode_has_owner_id(self): + self.assertTrue(self.investment_create.owner) diff --git a/investments/tests/test_views.py b/investments/tests/test_views.py new file mode 100644 index 000000000..82cbf0869 --- /dev/null +++ b/investments/tests/test_views.py @@ -0,0 +1,119 @@ +from rest_framework.test import APITestCase + +from model_bakery import baker +import ipdb + +from investments.models import Investment +from investments.serializers import InvestmentDetailSerializer + +class InvestmentTestViews(APITestCase): + + @classmethod + def setUpTestData(cls): + + cls.user_create = baker.make_recipe('users.new_user') + + cls.create_investment_uri = f'/api/investments/{cls.user_create.id}/' + + cls.get_investments_uri = '/api/investments/' + + cls.valid_investment = { + "amount": 1000, + "created_at": "2022-10-06" + } + + cls.invalid_investment_1 = { + "amount": -100, + "created_at": "2022-10-06" + } + + cls.invalid_investment_2 = { + "amount": 100, + "created_at": "06/07/2022" + } + + cls.invalid_investment_3 = { + "amount": 100, + "created_at": "2024-07-07" + } + + cls.valid_investment_1 = { + "amount": 1000, + "created_at": "2022-12-06" + } + + cls.response_post_create_investment = { + "amount": "1000.00", + "created_at": "2022-12-06", + "gains": "0.00", + "owner": cls.user_create.id, + "expected_balance": "0.00", + "initial_amount": "1000.00" + } + + cls.valid_withdrawn_date = { + "withdrawn_date": "2022-12-06" + } + + cls.invalid_withdrawn_date = { + "withdrawn_date": "2024-12-06" + } + + cls.valid_investment = Investment.objects.create(**cls.valid_investment, owner_id=cls.user_create.id) + + cls.get_investment_uri = f'/api/investment/{cls.valid_investment.id}/' + + cls.withdrawn_investment_uri = f'/api/investment/{cls.valid_investment.id}/withdrawn/' + + cls.delete_investment_uri = f'/api/investment/{cls.valid_investment.id}/' + + def test_can_register_investment(self): + response_post = self.client.post(self.create_investment_uri, data=self.valid_investment_1, format="json") + del response_post.data['id'] + self.assertEqual(self.response_post_create_investment, response_post.data) + self.assertEqual(response_post.status_code, 201) + + def test_cannot_register_negative_investment(self): + response_post = self.client.post(self.create_investment_uri, self.invalid_investment_1, format="json") + self.assertEqual(response_post.status_code, 400) + for key, value in response_post.data.items(): + self.assertEqual(value[0][:], f"{self.invalid_investment_1['amount']}.00 is a invalid number, please only positive numbers!") + + def test_cannot_register_investment_invalid_formate_date_1(self): + response_post = self.client.post(self.create_investment_uri, self.invalid_investment_2, format="json") + self.assertEqual(response_post.status_code, 400) + for key, value in response_post.data.items(): + self.assertEqual(value[0][:], "Date has wrong format. Use one of these formats instead: YYYY-MM-DD.") + + def test_cannot_register_investment_invalid_formate_date_2(self): + + response_post = self.client.post(self.create_investment_uri, self.invalid_investment_3, format="json") + self.assertEqual(response_post.status_code, 400) + + self.assertEqual(response_post.data, {"detail": "Invalid Date!"}) + + def test_cannot_register_empty_investment(self): + response_post = self.client.post(self.create_investment_uri, {}, format="json") + self.assertEqual(response_post.status_code, 400) + for key, value in response_post.data.items(): + self.assertEqual(value[0][:], "This field is required.") + + def test_can_list_investments(self): + response = self.client.get(self.get_investments_uri) + self.assertEqual(200, response.status_code) + self.assertEqual(response.data['count'], 1) + + def test_can_list_one_investment(self): + response = self.client.get(self.get_investment_uri) + self.assertEqual(200, response.status_code) + + def test_can_withdrawn_investment(self): + self.client.get(self.get_investment_uri) + response_withdrawn = self.client.post(self.withdrawn_investment_uri, self.valid_withdrawn_date, format="json") + self.assertEqual(response_withdrawn.status_code, 201) + self.assertEqual(response_withdrawn.data['withdrawn_date'], self.valid_withdrawn_date['withdrawn_date']) + + def test_can_safe_delete_investment(self): + response_delete = self.client.delete(self.delete_investment_uri) + self.assertEqual(response_delete.status_code, 204) + diff --git a/investments/views.py b/investments/views.py index b17c91e47..44a426721 100644 --- a/investments/views.py +++ b/investments/views.py @@ -1,9 +1,10 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics +from rest_framework.views import Response, status from datetime import date from decimal import Decimal + import ipdb -from uritemplate import partial from investments.models import Investment from investments.serializers import InvestmentDetailSerializer, InvestmentSerializer, InvestmentWithdrawnDetailSerializer @@ -15,11 +16,11 @@ def validate_dates(investment, today_date, investment_date): separeted_date = str(investment_date).split("/") - #Menos de 1 if int(today_separeted_date[2]) == int(separeted_date[2]): if int(today_separeted_date[1]) > int(separeted_date[1]): months = int(today_separeted_date[1]) - int(separeted_date[1]) + investment.initial_amount = investment.amount for _ in range(months): new_gains = Decimal(investment.amount) * Decimal(0.52/100) @@ -33,7 +34,6 @@ def validate_dates(investment, today_date, investment_date): return investment - #1 ano if int(today_separeted_date[2]) - int(separeted_date[2]) == 1 : if int(today_separeted_date[1]) == int(separeted_date[1]): @@ -45,6 +45,7 @@ def validate_dates(investment, today_date, investment_date): expected_balance = (new_gains + Decimal(investment.amount)) investment.gains = round(new_gains,2) + investment.initial_amount = investment.amount investment.amount = expected_balance investment.expected_balance = round(expected_balance + new_gains , 2) @@ -52,7 +53,6 @@ def validate_dates(investment, today_date, investment_date): return investment - #1-2 anos if ( int(today_separeted_date[2]) - int(separeted_date[2]) == 1 or @@ -62,15 +62,14 @@ def validate_dates(investment, today_date, investment_date): if int(today_separeted_date[1]) == int(separeted_date[1]): if int(today_separeted_date[0]) == int(separeted_date[0]): - print("1-2 1") new_gains = (years * 12) * (Decimal(investment.amount) * Decimal((0.52/100))) expected_balance = (new_gains + Decimal(investment.amount)) investment.gains = round(new_gains,2) + investment.initial_amount = investment.amount investment.amount = expected_balance investment.expected_balance = round(expected_balance + new_gains , 2) - # ipdb.set_trace() investment.save() @@ -84,6 +83,7 @@ def validate_dates(investment, today_date, investment_date): if int(today_separeted_date[0]) < int(separeted_date[0]): months = int(separeted_date[1]) - int(today_separeted_date[1]) + investment.initial_amount = investment.amount for _ in range(months - 1): @@ -114,7 +114,6 @@ def validate_dates(investment, today_date, investment_date): return investment - #+2anos if int(today_separeted_date[2]) - int(separeted_date[2]) > 2: if int(today_separeted_date[1]) == int(separeted_date[1]): @@ -138,6 +137,7 @@ def validate_dates(investment, today_date, investment_date): years = int(today_separeted_date[2]) - int(separeted_date[2]) if int(today_separeted_date[0]) < int(separeted_date[0]): + investment.initial_amount = investment.amount for _ in range(months - 1): @@ -178,13 +178,6 @@ class CreateInvestmentView(generics.CreateAPIView): lookup_url_kwarg = 'owner_id' - def perform_create(self, serializer): - owner_id = self.kwargs['owner_id'] - - owner = get_object_or_404(User, pk=owner_id) - - serializer.save(owner=owner) - class RetrieveUpdateDestroyInvestmentDetailView( generics.RetrieveDestroyAPIView ): @@ -211,9 +204,15 @@ def retrieve(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): - ... + investment_id = self.kwargs['investment_id'] + + investment = get_object_or_404(Investment, pk=investment_id) + + investment.isActive = False + + investment.save() -# mudar o delete para soft delete + return Response({}, status.HTTP_204_NO_CONTENT) class WithdrawnInvestmentView(generics.CreateAPIView): queryset = Investment.objects.all() diff --git a/investments_management/settings.py b/investments_management/settings.py index da6a2829d..12fb59544 100644 --- a/investments_management/settings.py +++ b/investments_management/settings.py @@ -147,7 +147,6 @@ REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, - 'EXCEPTION_HANDLER': 'utilities.api_exceptions.custom_exception_handler', 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } diff --git a/requirements.txt b/requirements.txt index af06039c6..15f694a9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ ipython==8.7.0 jedi==0.18.2 jsonschema==4.17.3 matplotlib-inline==0.1.6 +model-bakery==1.9.0 mypy-extensions==0.4.3 parso==0.8.3 pathspec==0.10.2 diff --git a/users/baker_recipes.py b/users/baker_recipes.py new file mode 100644 index 000000000..78c96985c --- /dev/null +++ b/users/baker_recipes.py @@ -0,0 +1,8 @@ +from model_bakery.recipe import Recipe +from users.models import User + +new_user = Recipe( + User, + email= "kenzinho@gmail.com", + username= "kenzinho" +) \ No newline at end of file diff --git a/users/migrations/0002_user_isactive.py b/users/migrations/0002_user_isactive.py new file mode 100644 index 000000000..a616a2b52 --- /dev/null +++ b/users/migrations/0002_user_isactive.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2022-12-07 11:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="isActive", + field=models.BooleanField(default=True), + ), + ] diff --git a/users/models.py b/users/models.py index 96528431e..eb07a63a0 100644 --- a/users/models.py +++ b/users/models.py @@ -5,3 +5,5 @@ class User(AbstractUser): id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False) email = models.CharField(max_length=127) + isActive = models.BooleanField(default=True) + diff --git a/users/serializers.py b/users/serializers.py index b28ba403a..7df21a861 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -1,12 +1,20 @@ from rest_framework import serializers -from rest_framework.exceptions import APIException +import ipdb from investments.serializers import InvestmentSerializer from users.models import User +from rest_framework.exceptions import APIException +from rest_framework.views import status -class Error(APIException): - status_code = 403 +class CustomValidation(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "A server error occured." + + def __init__(self, detail, status_code): + if status_code is not None:self.status_code=status_code + if detail is not None: + self.detail={"detail": detail} class UserSerializer(serializers.ModelSerializer): class Meta: @@ -15,19 +23,21 @@ class Meta: fields =[ "id", "email", - "username" + "username", + "isActive" ] - read_only_fields=["id"] + read_only_fields=["id", "isActive"] def create(self, validated_data): if User.objects.filter(email=validated_data["email"]).exists(): - raise Error({"message": "Email already been used!"}) + raise CustomValidation("Email aready in use!", 409) return User.objects.create_user(**validated_data) + class UserDetailSerializer(serializers.ModelSerializer): investments = InvestmentSerializer(many=True) class Meta: model = User - fields = [ "username","email", "investments"] \ No newline at end of file + fields = [ "username", "email", "investments", "isActive"] \ No newline at end of file diff --git a/users/tests.py b/users/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/users/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/users/tests/__init__.py b/users/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/users/tests/test_models.py b/users/tests/test_models.py new file mode 100644 index 000000000..2f5f71ade --- /dev/null +++ b/users/tests/test_models.py @@ -0,0 +1,22 @@ +from django.test import TestCase + +from users.models import User + +class UserTestModel(TestCase): + @classmethod + def setUp(self): + self.user_data = { + "email":"teste@gmail.com", + "username":"teste" + } + self.user = User.objects.create(**self.user_data) + + def test_username_max_length(self): + max_length = self.user._meta.get_field('email').max_length + + self.assertEqual(max_length, 127) + + def test_email_can_be_null(self): + nullable = self.user._meta.get_field("email").null + + self.assertFalse(nullable) \ No newline at end of file diff --git a/users/tests/test_views.py b/users/tests/test_views.py new file mode 100644 index 000000000..97fa37d22 --- /dev/null +++ b/users/tests/test_views.py @@ -0,0 +1,52 @@ +from rest_framework.test import APITestCase + +import ipdb + +from users.serializers import UserDetailSerializer, UserSerializer +from users.models import User + + +class UserTestViews(APITestCase): + + @classmethod + def setUpTestData(cls): + + cls.create_user_uri = "/api/accounts/" + + cls.valid_user = { + "username": "kenzinho", + "email": "kenzinho@kenzinho.com" + } + + cls.valid_user_1 = { + "username": "kenzinho1", + "email": "kenzinho@kenzinho1.com" + } + + cls.invalid_user = { + "username": "", + "email": "" + } + cls.owner_1 = User.objects.create_user(**cls.valid_user_1) + + + + def test_can_register_user(self): + response_post = self.client.post(self.create_user_uri, self.valid_user, format="json") + self.assertEqual(UserSerializer(instance=response_post.data).data, response_post.data) + self.assertEqual(response_post.status_code, 201) + + def test_cannot_register_user(self): + response_post = self.client.post(self.create_user_uri, {}, format="json") + self.assertEqual(response_post.status_code, 400) + for key, value in response_post.data.items(): + self.assertEqual(value[0][:], "This field is required.") + + def test_can_edit_user(self): + response_patch = self.client.patch(f"/api/accounts/{self.owner_1.id}/", {"username": "KenzinhoUpdated"}, format="json") + self.assertEqual(UserDetailSerializer(instance=response_patch.data).data, response_patch.data) + self.assertEqual(response_patch.status_code, 200) + + def test_can_safe_delete_user(self): + response_delete = self.client.delete(f"/api/accounts/{self.owner_1.id}/") + self.assertEqual(response_delete.status_code, 204) diff --git a/users/views.py b/users/views.py index 403673aa0..46d1a0872 100644 --- a/users/views.py +++ b/users/views.py @@ -1,4 +1,7 @@ +from django.shortcuts import get_object_or_404 from rest_framework import generics +from rest_framework.views import Response, status +import ipdb from users.models import User from users.serializers import UserDetailSerializer, UserSerializer @@ -13,3 +16,14 @@ class ListUpdateDeleteDetailUserView(generics.RetrieveUpdateDestroyAPIView): serializer_class = UserDetailSerializer lookup_field = "id" + + def destroy(self, request, *args, **kwargs): + user_id = self.kwargs['id'] + + user = get_object_or_404(User, pk=user_id) + + user.isActive = False + + user.save() + + return Response({}, status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/utilities/api_exceptions.py b/utilities/api_exceptions.py deleted file mode 100644 index 232ba4720..000000000 --- a/utilities/api_exceptions.py +++ /dev/null @@ -1,27 +0,0 @@ -from rest_framework.views import exception_handler -from rest_framework.exceptions import APIException - -def custom_exception_handler(exc, context): - - response = exception_handler(exc, context) - - if response is not None: - response.data['status_code'] = response.status_code - - #replace detail key with message key by delete detail key - response.data['message'] = response.data['detail'] - del response.data['detail'] - - return response - -class CustomApiException(APIException): - - #public fields - detail = None - status_code = None - - # create constructor - def __init__(self, status_code, message): - #override public fields - CustomApiException.status_code = status_code - CustomApiException.detail = message \ No newline at end of file From f548c5fd010a3a661619f40232dc814ee93b3f90 Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Wed, 7 Dec 2022 11:06:57 -0400 Subject: [PATCH 11/12] feat: can list one investment, paginated --- investments/urls.py | 3 ++- investments/views.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/investments/urls.py b/investments/urls.py index c1790b4b2..c760faa23 100644 --- a/investments/urls.py +++ b/investments/urls.py @@ -1,11 +1,12 @@ from django.urls import path -from investments.views import CreateInvestmentView, ListInvestmentView, RetrieveUpdateDestroyInvestmentDetailView, WithdrawnInvestmentView +from investments.views import CreateInvestmentView, ListInvestmentView, ListOneUserInvestmentsView, RetrieveUpdateDestroyInvestmentDetailView, WithdrawnInvestmentView urlpatterns=[ path('investments/', ListInvestmentView.as_view()), path('investments//', CreateInvestmentView.as_view()), + path('investments//management/', ListOneUserInvestmentsView.as_view()), path('investment//', RetrieveUpdateDestroyInvestmentDetailView.as_view()), path('investment//withdrawn/', WithdrawnInvestmentView.as_view()), diff --git a/investments/views.py b/investments/views.py index 44a426721..c6ff9d550 100644 --- a/investments/views.py +++ b/investments/views.py @@ -1,14 +1,16 @@ +from django.forms import model_to_dict from django.shortcuts import get_object_or_404 from rest_framework import generics from rest_framework.views import Response, status from datetime import date from decimal import Decimal +from django.core.paginator import Paginator import ipdb from investments.models import Investment from investments.serializers import InvestmentDetailSerializer, InvestmentSerializer, InvestmentWithdrawnDetailSerializer -from users.models import User + def validate_dates(investment, today_date, investment_date): @@ -171,6 +173,15 @@ class ListInvestmentView(generics.ListAPIView): queryset = Investment.objects.all() serializer_class = InvestmentSerializer +class ListOneUserInvestmentsView(generics.ListAPIView): + queryset = Investment.objects.all() + serializer_class = InvestmentSerializer + + lookup_url_kwarg = 'owner_id' + + def get_queryset(self): + return self.queryset.filter(owner_id=self.kwargs['owner_id']) + class CreateInvestmentView(generics.CreateAPIView): queryset = Investment.objects.all() From e96b4a61f87a269ea20e11d45c9a6560bdcbd4ca Mon Sep 17 00:00:00 2001 From: Cayo Nakasato Date: Wed, 7 Dec 2022 11:17:17 -0400 Subject: [PATCH 12/12] fix: fixed read.me --- README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a243e4b88..3a4007919 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ ``` - Python + - Django - Django Rest Framework - PostgreSQL - - UUID - - SQLite3 - - Ipdb ``` - +## Pré Requisitos: + -Python + -Pip + -PostgreSQL #### Assim que clonar o repositório entre na pasta com : - cd backend-test-nomedousuário - code . @@ -23,12 +24,17 @@ #### Para utilizar o venv se estiver utilizando Linux use o comando: source venv/bin/activate - #### Para utilizar o venv se estiver utilizando Windows use o comando: + + #### Para utilizar o venv se estiver utilizando bash use o comando: source venv/Scripts/activate + #### Para utilizar o venv se estiver utilizando PowerShell use o comando: + venv/Scripts/activate + ### Para instalar as dependências use o comando: pip install -r requirements.txt + ##### Dentro do arquivo requirements.txt está todos as denpendências que o projeto necessita para o código rodar normalmente. ## Após instalar pode se iniciar o projeto com @@ -36,7 +42,11 @@ python manage.py migrate ### Após rodar as migrations pode iniciar o projeto com: python manage.py runserver - ### Para acessar a documentação, após iniciar o servidor, possui a rota -## /api/docs -### Nesta rota acessando pelo localhost:****/api/docs terá acesso a documentação da api! + # Para acessar a documentação, após iniciar o servidor, possui a rota +Nesta rota acessando pelo localhost:****/api/docs terá acesso a documentação da api! +``` +/api/docs +```` + +