diff --git a/.env.example b/.env.example
new file mode 100644
index 000000000..6ced21934
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+POSTGRES_NAME=postgres
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=coderockr
+POSTGRES_HOST=host.docker.internal
+POSTGRES_PORT=5432
\ No newline at end of file
diff --git a/.flake8 b/.flake8
new file mode 100644
index 000000000..110313d61
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,5 @@
+[flake8]
+max-line-length=120
+ignore=E731
+ignore=C0114
+ignore=c0116
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..8aff6f037
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,115 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pipenv
+# According to pypa/pipenv#598, it is
+recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don’t work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# Sublime Text
+*.sublime-*
+
+# local env
+.local
+
+# PyCharm
+.idea/*
+
+# MacOS X
+.DS_Store
+
+# SQL
+*.sql
+
+# VSCODE
+.vscode/settings.json
+
+# poetry
+poetry.lock
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..312d83407
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,19 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Python: Django",
+ "type": "python",
+ "request": "launch",
+ "program": "${workspaceFolder}\\app\\manage.py",
+ "args": [
+ "runserver"
+ ],
+ "django": true,
+ "justMyCode": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..b65ef74b6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,19 @@
+init:
+ cp .env.example
+build:
+ docker-compose build --no-cache
+ docker-compose up -d
+ docker-compose logs -f
+rebuild:
+ docker-compose down --remove-orphans --volumes
+ sudo rm -rf data
+ make build
+run:
+ docker-compose down
+ docker-compose up -d
+ docker-compose logs -f
+down:
+ docker-compose down --remove-orphans --volumes
+
+terminal:
+ docker exec -it servi_app bash
\ No newline at end of file
diff --git a/README.md b/README.md
index ea8115e67..107e8a350 100644
--- a/README.md
+++ b/README.md
@@ -1,88 +1,132 @@
-# Back End Test Project
+# Coderockr Investments 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.
+An API for an application that stores and manages people and their investments, and send emails whenever an investment is created or withdrawal.
-## Scope
+## Initializing
-In this challenge you should build an API for an application that stores and manages investments, it should have the following features:
+### With Docker
-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.
+You need to have `docker` and `docker-compose` installed on your machine. for that check the proprietary documentation links: [Docker](https://docs.docker.com/engine/install/) e [Docker-compose](https://docs.docker.com/compose/install/), in that order.
-__NOTE:__ the implementation of an interface will not be evaluated.
+Then you should copy the data from `.env.example` to `.env`, you need to choose a **PASSWORD** and a **PORT** in the `.env` file. With docker the **HOST** must be `host.docker.internal`
-### Gain Calculation
+To install all packages and dependencies, run:
-The investment will pay 0.52% every month in the same day of the investment creation.
+```
+make build
+```
-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.
+Or, if you're using windows open the `Makefile` file and run the `build` block, line by line. To know more[leia](makefile).
-### Taxation
+Access http://localhost:8080 and you will see the service running.
-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.
+### Without Docker
-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).
+You need to have `Python 3.10^` installed on your machine. for that check the proprietary download [Link](https://www.python.org/downloads/)
-## 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.
+You need to have `PostgreSQL` installed on your machine. for that check the proprietary download [Link](https://www.postgresql.org/download/)
-## 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.
+Then you should copy the data from `.env.example` to `.env`, it is necessary to put the **PASSWORD** and the **PORT** chosen in postgreSQL to `.env` file. Without docker the **HOST** must be `localhost`
-## Coding Standards
-When working on the project be as clean and consistent as possible.
+To create the `Venv` file run:
-## Project Deadline
-Ideally you'd finish the test project in 5 days. It shouldn't take you longer than a entire week.
+```
+python -m venv venv
+```
-## Quality Assurance
-Use the following checklist to ensure high quality of the project.
+To activate `VirtualEnv` run:
-### 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?
+```
+./venv/scripts/activate
+```
-## Submission
-1. A link to the Github repository.
-2. Briefly describe how you decided on the tools that you used.
+To install `Poetry` run:
-## 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.
+```
+pip install poetry
+```
-## Credits
+To install all packages and dependencies, run:
-This coding challenge was inspired on [kinvoapp/kinvo-back-end-test](https://github.com/kinvoapp/kinvo-back-end-test/blob/2f17d713de739e309d17a1a74a82c3fd0e66d128/README.md)
+```
+poetry install
+```
-[gains]: #gain-calculation
-[taxes]: #taxation
-[interest]: #interest-calculation
-[compound gain]: https://www.investopedia.com/terms/g/gain.asp
+To run all migrations:
+
+```
+poetry run python ./app/manage.py makemigrations
+```
+```
+poetry run python ./app/manage.py migrate
+```
+
+Finally, run the server:
+
+```
+poetry run python ./app/manage.py runserver
+```
+
+Access http://localhost:8000 and you will see the service running.
+
+## Running Unit Tests
+
+First you need to initialize the app `Without Docker`.
+
+Then go to the `app` folder:
+```
+cd app
+```
+
+Finally run:
+```
+poetry run pytest
+```
+
+## Link to the API documentation
+
+**There are 2 different documentations**
+`Swagger:`
+ - With Swagger it's possible to test the endpoints directly from this documentation, it makes testing a lot easier. If you're running in **docker**, access the link **http://localhost:8080**.
+ **Without Docker**, access the link **http://localhost:8000**
+
+`Redoc:`
+ - Redoc is user-friendly and perfect to use on a daily basis and facilitate API absorption. If you're running in **docker**, access the link **http://localhost:8080/redoc**.
+ **Without Docker**, access the link **http://localhost:8000/redoc**
+
+
+
+## List of third-party libraries used
+
+### Docker
+Docker makes it easy to run the application without having to put in a lot of effort. With application development using Docker, you don’t need to install a bunch of language environments on your system. You can simply run the application inside docker container with the help of a image.
+
+### Python
+
+Python is an extremely powerful and versatile programming language in terms of the types of applications you can create.
+
+### Django and Django-RestFramework
+
+Django is a open source framework that is compatible with major operating systems and databases. It has a great number of advantages. It's considered a developer-friendly framework easy to pick up. It provides robust security features, helps to avoid the common mistakes of web development including SQL injection, clickjacking, cross-site request forgery and scripting.
+
+Django REST framework (DRF) is a open source, mature and well supported Python/Django library that aims at building sophisticated web APIs. It is flexible and fully-featured toolkit with modular and customizable architecture that makes possible development of both simple, turn-key API endpoints and complicated REST constructs.
+
+### Poetry
+
+Poetry is a tool for dependency management and packaging in Python. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you. Poetry offers a lockfile to ensure repeatable installs, and can build your project for distribution.
+
+### drf_yasg
+
+drf_yasg is a API doc generation tool which provides the option to choose between swagger-ui and redoc or both for generating documentation for your APIs
+
+### NumPy
+
+NumPy arrays are faster and more compact than Python lists. An array consumes less memory and is convenient to use. NumPy uses much less memory to store data and it provides a mechanism of specifying the data types. **In this case it was used to calculate the difference of months between two dates**
+
+### Pandas
+
+Pandas is an open-source Python library designed to deal with data analysis and data manipulation. It is built on top of **NumPy** and it has several functions for cleaning, analyzing, and manipulating data, which can help you extract valuable insights about your data set. **In this case it was used to prepare the dates for later calculations.**
+
+### PostgreSQL
+
+PostgreSQL comes with many features aimed to help developers build applications, administrators to protect data integrity and build fault-tolerant environments, and help you manage your data no matter how big or small the dataset. In addition to being free and open source, PostgreSQL is highly extensible.
diff --git a/app/core/__init__.py b/app/core/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/core/asgi.py b/app/core/asgi.py
new file mode 100644
index 000000000..76ae1d870
--- /dev/null
+++ b/app/core/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for core 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', 'core.settings')
+
+application = get_asgi_application()
diff --git a/app/core/router.py b/app/core/router.py
new file mode 100644
index 000000000..4609485d2
--- /dev/null
+++ b/app/core/router.py
@@ -0,0 +1,9 @@
+from rest_framework import routers
+from persons.views import PersonViewSet
+from investments.views import InvestmentViewSet, WithdrawnViewSet
+
+
+router = routers.DefaultRouter()
+router.register('persons', PersonViewSet)
+router.register('investments', InvestmentViewSet)
+router.register('investments', WithdrawnViewSet)
\ No newline at end of file
diff --git a/app/core/settings.py b/app/core/settings.py
new file mode 100644
index 000000000..f5cd91b44
--- /dev/null
+++ b/app/core/settings.py
@@ -0,0 +1,174 @@
+"""
+Django settings for core project.
+
+Generated by 'django-admin startproject' using Django 4.1.2.
+
+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/
+"""
+import os
+from pathlib import Path
+
+from dotenv import load_dotenv
+
+load_dotenv(dotenv_path=".env")
+
+# .Env Variables
+POSTGRES_NAME = os.getenv("POSTGRES_NAME")
+POSTGRES_USER = os.getenv("POSTGRES_USER")
+POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")
+POSTGRES_HOST = os.getenv("POSTGRES_HOST")
+POSTGRES_PORT = os.getenv("POSTGRES_PORT")
+
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+EMAIL_HOST = 'smtp.gmail.com'
+EMAIL_PORT = '587'
+EMAIL_HOST_USER = 'investmentscoderockr@gmail.com'
+EMAIL_HOST_PASSWORD = 'pvcxleglfznmtgvk'
+EMAIL_USE_TLS = True
+EMAIL_USE_SSL = False
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'django-insecure-eka=r2^l_-93b0**4_iv2&l*de0r%#am)%7&t9#$1+6)ngvooi'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = [
+ '127.0.0.1',
+ 'host.docker.internal',
+ 'localhost',
+]
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ # installed apps
+ 'rest_framework',
+ 'django_filters',
+ 'drf_yasg',
+ #myapps
+ 'investments',
+ 'persons',
+]
+
+REST_FRAMEWORK = {
+ 'DEFAULT_PERMISSION_CLASSES': (
+ 'rest_framework.permissions.AllowAny',
+ ),
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+ 'PAGE_SIZE': 5
+}
+
+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 = 'core.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 = 'core.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
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql_psycopg2',
+ 'NAME': POSTGRES_NAME,
+ 'USER': POSTGRES_USER,
+ 'PASSWORD': POSTGRES_PASSWORD,
+ 'HOST': POSTGRES_HOST,
+ 'PORT': POSTGRES_PORT,
+ }
+}
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/4.1/howto/static-files/
+STATIC_URL = 'static/'
+STATIC_ROOT = BASE_DIR / 'staticfiles'
+
+MEDIA_URL = 'media/'
+MEDIA_ROOT = BASE_DIR / 'media'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+LOGIN_URL = "/admin/login/"
diff --git a/app/core/urls.py b/app/core/urls.py
new file mode 100644
index 000000000..6c5cd5b40
--- /dev/null
+++ b/app/core/urls.py
@@ -0,0 +1,43 @@
+"""core 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
+from rest_framework import permissions
+from drf_yasg import openapi
+from drf_yasg.views import get_schema_view
+from django.urls import include
+
+from core.router import router
+
+schema_view = get_schema_view(
+ openapi.Info(
+ title="Coderockr Investments",
+ default_version='v1',
+ description="Investments Api Documentation - Coderockr",
+ contact=openapi.Contact(email="gabriellasoares2@gmail.com"),
+ ),
+ public=False,
+ permission_classes=[permissions.AllowAny],
+)
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path('', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
+ path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
+ path('api/V1/', include(router.urls)),
+]
+
diff --git a/app/core/wsgi.py b/app/core/wsgi.py
new file mode 100644
index 000000000..63bb09994
--- /dev/null
+++ b/app/core/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for core 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', 'core.settings')
+
+application = get_wsgi_application()
diff --git a/app/investments/__init__.py b/app/investments/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/investments/admin.py b/app/investments/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/app/investments/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/app/investments/apps.py b/app/investments/apps.py
new file mode 100644
index 000000000..bec2521a0
--- /dev/null
+++ b/app/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/app/investments/migrations/__init__.py b/app/investments/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/investments/models.py b/app/investments/models.py
new file mode 100644
index 000000000..be201f21f
--- /dev/null
+++ b/app/investments/models.py
@@ -0,0 +1,37 @@
+from django.core.mail import send_mail
+from django.db import models
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+import pandas as pd
+from datetime import datetime
+
+from persons.models import Person
+
+
+class Investment(models.Model):
+ owner = models.ForeignKey(Person, related_name='investments', blank=True, null=True,on_delete=models.CASCADE)
+ creation_date = models.DateField()
+ initial_amount = models.FloatField()
+ withdrawn_date = models.DateField(blank=True, null=True)
+ active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return f'{self.owner}'
+
+ class Meta:
+ verbose_name = 'Investment'
+
+
+@receiver(post_save, sender=Investment)
+def send_mail_on_create(sender, instance=None, created=False, **kwargs):
+ if created:
+ initial_amount = instance.initial_amount
+ df = pd.DataFrame(data={'date':[datetime.strptime(str(instance.creation_date), '%Y-%m-%d')]})
+ day = df.date.dt.day[0]
+ name = instance.owner.name
+ email = send_mail(
+ 'You have a new investment',
+ f'Hi {name}, \n\nYou have a new investment in your name. The initial amount is ${initial_amount}, this value increases by 0.52% every month on day {day}. \nThank you for the trust, have a great day',
+ 'investmentscoderockr@outlook.com',
+ [instance.owner.email],
+ )
diff --git a/app/investments/serializers.py b/app/investments/serializers.py
new file mode 100644
index 000000000..6b498a5f6
--- /dev/null
+++ b/app/investments/serializers.py
@@ -0,0 +1,70 @@
+from datetime import datetime
+
+import numpy as np
+import pandas as pd
+from rest_framework import serializers
+
+from investments.models import Investment
+
+
+class InvestmentSerializer(serializers.ModelSerializer):
+ expected_balance = serializers.SerializerMethodField()
+ withdrawn_balance = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Investment
+ fields = ('id', 'owner', 'creation_date', 'initial_amount', 'expected_balance', 'withdrawn_balance', 'withdrawn_date', 'active')
+ extra_kwargs = {
+ 'active': {'read_only': True},
+ 'expected_balance': {'read_only': True},
+ 'withdrawn_date': {'read_only': True},
+ 'owner': {'required': True},
+ }
+
+ def calculate_number_of_months(self, investment):
+ date_now = datetime.today()
+ creation_date = datetime.strptime(str(investment.creation_date), '%Y-%m-%d')
+ withdrawn_date = investment.withdrawn_date
+ if withdrawn_date is None:
+ data = {'date1': [date_now], 'date2': [creation_date]}
+ else:
+ withdrawn_date = datetime.strptime(str(withdrawn_date), '%Y-%m-%d')
+ data = {'date1': [withdrawn_date], 'date2': [creation_date]}
+ df = pd.DataFrame(data=data)
+ df['nb_months'] = ((df.date1 - df.date2)/np.timedelta64(1, 'M'))
+ df['nb_months'] = df['nb_months'].astype(int)
+
+ return df['nb_months'].item()
+
+ def get_expected_balance(self, investment):
+ nb_months = self.calculate_number_of_months(investment)
+ initial_amount = investment.initial_amount
+ expected_balance = round(float(initial_amount * (pow((1 + 0.52 / 100), nb_months))), 2)
+
+ return expected_balance
+
+ def get_withdrawn_balance(self, investment):
+ expected_balance = self.get_expected_balance(investment)
+ nb_months = self.calculate_number_of_months(investment)
+ initial_amount = investment.initial_amount
+ profit = expected_balance - initial_amount
+ if nb_months < 12:
+ return round(float((profit * 0.775) + initial_amount), 2)
+ elif nb_months >= 12 and nb_months < 24:
+ return round(float((profit * 0.815) + initial_amount), 2)
+ else:
+ return round(float((profit * 0.85) + initial_amount), 2)
+
+
+class WithdrawnSerializer(InvestmentSerializer):
+ class Meta:
+ model = Investment
+ fields = ('id', 'owner', 'creation_date', 'initial_amount', 'expected_balance', 'withdrawn_balance', 'withdrawn_date', 'active')
+ extra_kwargs = {
+ 'active': {'read_only': True},
+ 'expected_balance': {'read_only': True},
+ 'initial_amount': {'read_only': True},
+ 'creation_date': {'read_only': True},
+ 'owner': {'read_only': True},
+ 'withdrawn_date': {'required': True},
+ }
diff --git a/app/investments/tests/__init__.py b/app/investments/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/investments/tests/test_investments_api.py b/app/investments/tests/test_investments_api.py
new file mode 100644
index 000000000..6a94dfd72
--- /dev/null
+++ b/app/investments/tests/test_investments_api.py
@@ -0,0 +1,158 @@
+from urllib import response
+from investments.tests.test_investments_base import InvestmentsTestBase
+from datetime import date, timedelta, datetime
+
+class InvestmentsAPIv1Test(InvestmentsTestBase):
+ BASE_URL = 'http://127.0.0.1:8000/api/V1/investments/'
+
+ def test_person_post_returns_status_code_201(self):
+ response = self.client.post(
+ 'http://127.0.0.1:8000/api/V1/persons/',
+ data={
+ 'email': 'example@example.com',
+ 'name': 'Teste'
+ })
+ return response.data.get('id')
+
+ def test_investment_list_returns_status_code_200(self):
+ response = self.client.get(self.BASE_URL)
+ self.assertEqual(response.status_code, 200)
+
+ def test_investment_list_has_pagination(self):
+ wanted_investments = 100
+ self.create_investment_in_batch(qtd=wanted_investments)
+
+ response = self.client.get(self.BASE_URL)
+
+ assert response.data.get('next') is not None
+
+ return {"response": response.data, "wanted_investments": wanted_investments}
+
+
+ def test_investments_post_returns_status_code_201(self):
+ person = self.test_person_post_returns_status_code_201()
+
+ response = self.client.post(
+ self.BASE_URL,
+ data={
+ 'owner': person,
+ 'creation_date': '2019-10-22',
+ 'initial_amount': 10000
+ }
+ )
+
+ self.assertEqual(
+ response.status_code,
+ 201
+ )
+
+ return response.data
+
+ def test_investment_cannot_be_created_without_an_owner(self):
+ response = self.client.post(self.BASE_URL)
+
+ self.assertEqual(
+ response.status_code,
+ 400
+ )
+
+ def test_investments_creation_date_cannot_be_in_the_future(self):
+ person = self.test_person_post_returns_status_code_201()
+ today_date = date.today()
+ future_date = today_date + timedelta(1)
+
+ response = self.client.post(
+ self.BASE_URL,
+ data={
+ 'owner': person,
+ 'creation_date': future_date,
+ 'initial_amount': 10000
+ }
+ )
+
+ self.assertEqual(
+ response.status_code,
+ 400
+ )
+
+ def test_investment_initial_amount_cannot_be_negative(self):
+ person = self.test_person_post_returns_status_code_201()
+ response = self.client.post(
+ self.BASE_URL,
+ data={
+ 'owner': person,
+ 'creation_date': '2019-10-22',
+ 'initial_amount': -2
+ }
+ )
+ self.assertEqual(
+ response.status_code,
+ 400
+ )
+
+ def test_investment_withdrawal_returns_status_code_200(self):
+ investment = self.test_investments_post_returns_status_code_201()
+ investment_id = investment['id']
+
+ response = self.client.put(f'{self.BASE_URL}{investment_id}/withdrawn/', data={'withdrawn_date': '2022-10-22'})
+
+ self.assertEqual(
+ response.status_code,
+ 200
+ )
+
+ return investment
+
+ def test_investment_cannot_have_more_than_one_withdrawal(self):
+ investment = self.test_investment_withdrawal_returns_status_code_200()
+ investment_id = investment['id']
+
+ response = self.client.put(f'{self.BASE_URL}{investment_id}/withdrawn/', data={'withdrawn_date': '2022-10-22'})
+
+ self.assertEqual(
+ response.status_code,
+ 400
+ )
+
+ def test_investments_withdrawal_date_cannot_be_before_it_creation_date(self):
+ investment = self.test_investments_post_returns_status_code_201()
+
+ investment_id = investment['id']
+ creation_date = datetime.strptime(str(investment['creation_date']), '%Y-%m-%d')
+ withdrawn_date = (creation_date - timedelta(1)).date()
+
+ response = self.client.put(f'{self.BASE_URL}{investment_id}/withdrawn/', data={'withdrawn_date': withdrawn_date})
+
+ self.assertEqual(
+ response.status_code,
+ 400
+ )
+
+ def test_investments_withdrawal_date_cannot_be_in_the_future(self):
+ investment = self.test_investments_post_returns_status_code_201()
+
+ investment_id = investment['id']
+ now = datetime.today()
+ withdrawn_date = (now + timedelta(1)).date()
+
+ response = self.client.put(f'{self.BASE_URL}{investment_id}/withdrawn/', data={'withdrawn_date': withdrawn_date})
+ self.assertEqual(
+ response.status_code,
+ 400
+ )
+
+ def test_person_investments_list_return_all_investments(self):
+ investments = self.test_investment_list_has_pagination()
+ person_id = investments.get('response').get('results')[0].get('owner')
+ wanted_investments = investments.get('wanted_investments')
+
+ response = self.client.get(f'http://127.0.0.1:8000/api/V1/persons/{person_id}/investments/')
+
+ self.assertEqual(
+ response.data.get('count'),
+ wanted_investments
+ )
+
+
+
+
\ No newline at end of file
diff --git a/app/investments/tests/test_investments_base.py b/app/investments/tests/test_investments_base.py
new file mode 100644
index 000000000..6259d69e1
--- /dev/null
+++ b/app/investments/tests/test_investments_base.py
@@ -0,0 +1,29 @@
+from random import randrange
+from rest_framework.test import APITestCase
+from investments.models import Investment
+from persons.models import Person
+
+class InvestmentsMixin:
+ def create_person(self, email, name):
+ return Person.objects.create(email=email, name=name)
+
+ def create_investment(self, owner, creation_date, initial_amount):
+ return Investment.objects.create(owner=owner, creation_date=creation_date, initial_amount=initial_amount)
+
+ def create_investment_in_batch(self, qtd=0):
+ investments = []
+ person = self.create_person(email='teste@example.com', name='Teste')
+ for i in range(qtd):
+ kwargs = {
+ 'owner': person,
+ 'creation_date': '2019-10-22',
+ 'initial_amount': randrange(10000, 50000)
+ }
+ investment = self.create_investment(**kwargs)
+ investments.append(investment)
+ return investments
+
+
+class InvestmentsTestBase(APITestCase, InvestmentsMixin):
+ def setUp(self) -> None:
+ return super().setUp()
diff --git a/app/investments/views.py b/app/investments/views.py
new file mode 100644
index 000000000..c993aa9f6
--- /dev/null
+++ b/app/investments/views.py
@@ -0,0 +1,87 @@
+from datetime import datetime
+
+from django.core.mail import send_mail
+from rest_framework import mixins, status, viewsets
+from rest_framework.decorators import action
+from rest_framework.response import Response
+
+from investments.models import Investment
+from investments.serializers import InvestmentSerializer, WithdrawnSerializer
+
+
+class InvestmentViewSet(
+ mixins.CreateModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.ListModelMixin,
+ viewsets.GenericViewSet):
+ queryset = Investment.objects.all().order_by('-id')
+ serializer_class = InvestmentSerializer
+
+ def create(self, request):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ try:
+ datetime.strptime(request.data.get('creation_date'), '%Y-%m-%d')
+ except:
+ return Response('Please, enter a correct date. Ex: 2022-10-22', status=status.HTTP_400_BAD_REQUEST)
+
+ # valid creation date
+ if request.data.get('creation_date') is None:
+ return Response('The creation date is mandatory.', status=status.HTTP_400_BAD_REQUEST)
+ creation_date = datetime.strptime(request.data.get('creation_date'), '%Y-%m-%d')
+ now = datetime.today()
+ diff = (now - creation_date).days
+ if(diff < 0):
+ return Response('The creation date of an investment can be today or a date in the past.', status=status.HTTP_400_BAD_REQUEST)
+
+ #valid amout
+ initial_amount = request.data.get('initial_amount')
+ if(int(initial_amount) < 0):
+ return Response('The initial amount needs to be positive.', status=status.HTTP_400_BAD_REQUEST)
+
+ self.perform_create(serializer)
+ headers = self.get_success_headers(serializer.data)
+ return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+
+
+class WithdrawnViewSet(mixins.UpdateModelMixin,viewsets.GenericViewSet):
+ queryset = Investment.objects.all().order_by('-id')
+ serializer_class = WithdrawnSerializer
+
+ def send_email_on_withdrawn(self, investment,serializer):
+ initial_amount = investment.initial_amount
+ withdrawn_balance = serializer.get_withdrawn_balance(investment)
+ name = investment.owner.name
+ email = send_mail(
+ 'You have a new investment',
+ f'Hi {name}, \n\nYour investment has been withdrawn. The initial investment amount was {initial_amount} and the amount withdrawn was {withdrawn_balance}. \nThank you for the trust, have a great day',
+ 'investmentscoderockr@outlook.com',
+ [investment.owner.email],
+ )
+
+ @action(detail=True, methods=['put'])
+ def withdrawn(self, request, pk=None):
+ try:
+ datetime.strptime(request.data['withdrawn_date'], '%Y-%m-%d')
+ except:
+ return Response('Please, enter a correct date. Ex: 2022-10-22', status=status.HTTP_400_BAD_REQUEST)
+ investments = self.get_object()
+ serializer = WithdrawnSerializer(investments, data=request.data)
+ withdrawn_date = datetime.strptime(request.data['withdrawn_date'], '%Y-%m-%d')
+ creation_date = datetime.strptime(str(investments.creation_date), '%Y-%m-%d')
+ now = datetime.today()
+ diff_now_withdrawn_date = (now - withdrawn_date).days
+ diff_creation_date_withdrawn_date = (withdrawn_date - creation_date).days
+ if investments.active == False:
+ return Response('The withdrawal of this investment has already been made.', status=status.HTTP_400_BAD_REQUEST)
+ elif(diff_now_withdrawn_date < 0 or diff_creation_date_withdrawn_date < 0):
+ return Response('Withdrawals can happen in the past or today, but cannot happen before the investment creation or the future.', status=status.HTTP_400_BAD_REQUEST)
+ serializer.is_valid(raise_exception=True)
+ investments.active = False
+ self.send_email_on_withdrawn(investments, serializer)
+ serializer.save()
+ return Response(serializer.data)
+
+
+
\ No newline at end of file
diff --git a/app/manage.py b/app/manage.py
new file mode 100644
index 000000000..f2a662cfd
--- /dev/null
+++ b/app/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', 'core.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/app/persons/__init__.py b/app/persons/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/persons/admin.py b/app/persons/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/app/persons/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/app/persons/apps.py b/app/persons/apps.py
new file mode 100644
index 000000000..825dc19ee
--- /dev/null
+++ b/app/persons/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class UsersConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'persons'
diff --git a/app/persons/migrations/__init__.py b/app/persons/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/persons/models.py b/app/persons/models.py
new file mode 100644
index 000000000..6b25b8245
--- /dev/null
+++ b/app/persons/models.py
@@ -0,0 +1,11 @@
+from django.db import models
+
+class Person(models.Model):
+ name = models.CharField(max_length=256)
+ email = models.EmailField('Email Adress', unique=True)
+
+ def __str__(self):
+ return f'{self.name}'
+
+ class Meta:
+ verbose_name = 'User'
diff --git a/app/persons/serializers.py b/app/persons/serializers.py
new file mode 100644
index 000000000..40e053222
--- /dev/null
+++ b/app/persons/serializers.py
@@ -0,0 +1,8 @@
+from rest_framework import serializers
+from persons.models import Person
+
+
+class PersonSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Person
+ fields = ('id', 'email', 'name')
diff --git a/app/persons/views.py b/app/persons/views.py
new file mode 100644
index 000000000..3e80c22bc
--- /dev/null
+++ b/app/persons/views.py
@@ -0,0 +1,27 @@
+from investments.serializers import InvestmentSerializer
+from rest_framework import mixins, viewsets
+from rest_framework.decorators import action
+from rest_framework.response import Response
+
+from persons.models import Person
+from persons.serializers import PersonSerializer
+
+
+class PersonViewSet(
+ mixins.CreateModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.ListModelMixin,
+ viewsets.GenericViewSet):
+ queryset = Person.objects.all().order_by('-id')
+ serializer_class = PersonSerializer
+
+ @action(detail=True, methods=['get'])
+ def investments(self, request, pk=None, *args, **kwargs):
+ person = self.get_object()
+ page = self.paginate_queryset(person.investments.all().order_by('-id'))
+ if page is not None:
+ serializer = InvestmentSerializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+
+ serializer = self.get_serializer(person.investments.all(), many=True)
+ return Response(serializer.data)
diff --git a/app/pytest.ini b/app/pytest.ini
new file mode 100644
index 000000000..dfd92f93b
--- /dev/null
+++ b/app/pytest.ini
@@ -0,0 +1,11 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = core.settings
+python_files = test.py tests.py test_*.py tests_*.py *_test.py *_tests.py
+addopts =
+ --doctest-modules
+ --strict-markers
+ # -rP
+markers =
+ slow: Run tests that are slow
+ fast: Run fast tests
+ functional_test: Run tests that are selenium based
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..2cd89d963
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,28 @@
+version: '3.7'
+
+services:
+ db:
+ environment:
+ - POSTGRES_USER=${POSTGRES_USER}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
+ - POSTGRES_DB=${POSTGRES_NAME}
+ image: postgres
+ restart: always
+ ports:
+ - '${POSTGRES_PORT}:5432'
+ volumes:
+ - /var/lib/postgresql
+ admin:
+ image: fenglc/pgadmin4
+ ports:
+ - '5050:5050'
+ environment:
+ - DEFAULT_USER=admin
+ application:
+ build:
+ context: .
+ dockerfile: ./docker/Dockerfile
+ ports:
+ - '8080:8080'
+ depends_on:
+ - 'db'
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 000000000..e91ccc80c
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,14 @@
+FROM python:3.10
+
+WORKDIR /usr/src/app
+
+COPY .env /usr/src/app
+COPY pyproject.toml /usr/src/app
+RUN pip install poetry
+RUN poetry install
+
+
+COPY ./app /usr/src/app
+
+
+CMD [ "poetry", "run", "python", "/usr/src/app/manage.py", "runserver", "0.0.0.0:8080"]
\ No newline at end of file
diff --git a/poetry.toml b/poetry.toml
new file mode 100644
index 000000000..53b35d370
--- /dev/null
+++ b/poetry.toml
@@ -0,0 +1,3 @@
+[virtualenvs]
+create = true
+in-project = true
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..a799e37ab
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,30 @@
+[tool.poetry]
+name = "CodeRockr"
+version = "0.1.0"
+description = ""
+authors = ["gabriella "]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+Django = "^4.1"
+python-dotenv = "^0.20.0"
+psycopg2 = "^2.9.3"
+djangorestframework = "^3.13.1"
+Pillow = "^9.2.0"
+django-filter = "^22.1"
+rest_condition = "^1.0.3"
+drf-yasg = "^1.21.4"
+numpy = "^1.23.4"
+pandas = "^1.5.0"
+ptw = "^1.0.1"
+pytest-django = "^4.5.2"
+
+[tool.poetry.dev-dependencies]
+mypy = "^0.971"
+flake8 = "^5.0.4"
+pytest = "^7.1.2"
+pytest-django = "^4.5.2"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"