Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f80331d
initial
PabloNeri66 Dec 22, 2025
dd1be8a
Feat: Users ListCreateView
PabloNeri66 Dec 22, 2025
309bf3a
feat: endpoint users/register
PabloNeri66 Dec 22, 2025
6ca1d07
feat:model-admin entidades
PabloNeri66 Dec 22, 2025
8a19a22
feat: lógica business
PabloNeri66 Dec 22, 2025
e8ce46d
Feat:docstring no business
PabloNeri66 Dec 22, 2025
1d04b55
Feat: docstring no business
PabloNeri66 Dec 22, 2025
769b03a
Feat: ListCreateView
PabloNeri66 Dec 22, 2025
3dce975
feat: url investiment-list-create
PabloNeri66 Dec 22, 2025
93555f1
fix: queryset listcreateview
PabloNeri66 Dec 22, 2025
6f72a5d
Crud De Investidores(tabela auxiliar)
PabloNeri66 Dec 26, 2025
9178c75
Feat: ListCreate Investiment; alinhando urls
PabloNeri66 Dec 26, 2025
4b5ecfc
Feat: Investiment UpdateDeleteDetail e View de Saque
PabloNeri66 Dec 26, 2025
10a1f30
Fix: defer incorreto
PabloNeri66 Dec 26, 2025
ca4eecd
Testes Regra de negócio
PabloNeri66 Dec 26, 2025
109f49f
Feat: Servico de envio Smtp
PabloNeri66 Dec 26, 2025
f9829b1
COnfig Emai
PabloNeri66 Dec 26, 2025
1a7526f
Feat: Pagination
PabloNeri66 Dec 26, 2025
dc58343
Feat: email Signal config
PabloNeri66 Dec 26, 2025
50773c7
Feat: Log basico
PabloNeri66 Dec 26, 2025
33f678e
Feat: Docs Api
PabloNeri66 Dec 27, 2025
e7619e0
Fix:Docs
PabloNeri66 Dec 27, 2025
f432942
Format: Ruff Format Code
PabloNeri66 Dec 27, 2025
e91a1cc
Docs e Reqs
PabloNeri66 Dec 27, 2025
e03c51b
Obs e Carta de Apresentação
PabloNeri66 Dec 27, 2025
8b3b26d
Carta de apresentação
PabloNeri66 Dec 27, 2025
3dd95bf
Carta definitiva
PabloNeri66 Dec 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions carta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
Olá, pessoal.

Realizei este teste de Backend, no qual construí uma API seguindo os requisitos propostos no sistema/caso apresentado.

Utilizei a linguagem Python em conjunto com o framework Django e Django Rest Framework (DRF), estruturando o projeto no padrão MVT.

A escolha do Django se deu principalmente pela possibilidade de implementar um sistema com um usuário genérico desacoplado da entidade Investidor, aproveitando o User padrão fornecido pelo framework, bem como sua interface administrativa nativa.

Além disso, o ORM e os querysets do Django auxiliam significativamente no desempenho das consultas, especialmente considerando as regras de negócio envolvidas. Em um cenário de maior complexidade, seria possível otimizar ainda mais as queries, incluindo joins entre tabelas de forma eficiente.

Os serializers do Django foram utilizados como responsáveis pelo tratamento e validação dos dados, o que contribui diretamente para a produtividade do desenvolvedor e para a segurança da aplicação.

Os Signals (gatilhos) do Django foram empregados como parte essencial da integração com o serviço externo de SMTP, conforme solicitado nos requisitos.

Portanto, a escolha da stack não se deu apenas por afinidade técnica ou aprendizado contínuo, mas também pela produtividade, organização e nível de abstração que o Django oferece.

As entidades foram divididas em:

- Usuários

- Investidores

- Investimentos

Foi implementado o **CRUD** completo para Investidores e Investimentos.

No caso dos investimentos, priorizei a segurança e a integridade dos registros, utilizando uma flag booleana para indicar o encerramento, evitando alterações indevidas em registros históricos.

Como melhorias futuras, considero a implementação de permissões e autenticação mais refinadas, garantindo o uso adequado de cada endpoint. Além disso, seria interessante a criação de uma entidade Wallet, responsável por consolidar os investimentos, métricas e regras de negócio relacionadas.

Também utilizei bibliotecas auxiliares, todas devidamente documentadas no arquivo de requirements e na documentação do projeto.

Para padronização e formatação do código, utilizei o Ruff, visando manter um padrão de leitura consistente, tento sempre seguir padrão python, mas tenho costumes de aspas e outros toques.
Para automação de tarefas, utilizei o Taskipy, facilitando a execução de comandos recorrentes. Para documentação, utilizei o MkDocs, buscando uma melhor organização e visualização dos arquivos Markdown.

As variáveis de ambiente foram devidamente protegidas e configuradas por meio de um arquivo .env local.

Fico à disposição para receber feedbacks. Gostaria muito de ouvir a opinião de vocês sobre pontos de melhoria e aspectos positivos do projeto.

Agradeço pela oportunidade, Prazer!
115 changes: 115 additions & 0 deletions code/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# 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/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# dotenv
.env

# virtualenv
.venv
venv/
ENV/
.vscode
# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

.DS_Store
*.sqlite3
media/
*.pyc
*.db
*.pid

# Ignore Django Migrations in Development if you are working on team

# Only for Development only
# **/migrations/**
# !**/migrations
# !**/migrations/__init__.py
Empty file added code/business/__init__.py
Empty file.
146 changes: 146 additions & 0 deletions code/business/investiments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import datetime
from decimal import ROUND_HALF_EVEN, Decimal
from typing import Optional

from django.utils import timezone


class InvestimentBusiness:
"""Lógica de Negócio do Investimento

O investimento renderá 0,52% todos os meses,
no mesmo dia em que for realizado.
Dado que o ganho é pago mensalmente, deve ser tratado como ganho composto,
o que significa que a cada novo período (mês)
o valor ganho passará a fazer parte do saldo do investimento para
o próximo pagamento.

"""

MONTHLY_RATE = Decimal('0.0052')

def __init__(
self,
investiment_value: Decimal,
investiment_created_at: datetime.datetime,
withdrawn_created_at: Optional[datetime.datetime] = None,
):
self.investiment_value = investiment_value
self.investiment_created_at = investiment_created_at
self.withdrawn_created_at = withdrawn_created_at

def calculate_amount(self) -> Decimal:
"""
Calculo do Montante de um investimento aberto(data atual).
"""
initial_date = self.investiment_created_at.date()
today = timezone.localdate()

months = self._full_months_between(initial_date, today)
amount = (
self.investiment_value
* ( # Juros composto
Decimal('1') + self.MONTHLY_RATE
)
** months
)

return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)

def calculate_gains(self) -> Decimal:
"""
Cálculo dos ganhos (montante - investido) em um investimento
aberto(Data atual).
"""
amount = self.calculate_amount()
gains = amount - self.investiment_value

return gains.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)

def calculate_amount_withdrawn(self) -> Decimal:
"""
Saldo Montante em um investimento fechado.
"""
initial_date = self.investiment_created_at.date()
withdrawn_date = self.withdrawn_created_at.date()

months = self._full_months_between(initial_date, withdrawn_date)

amount = (
self.investiment_value
* (Decimal('1') + self.MONTHLY_RATE) ** months
)

return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)

def calculate_gains_withdrawn(self) -> Decimal:
"""Calculo de Ganho em um investimento Fechado."""
amount = self.calculate_amount_withdrawn()
gains = amount - self.investiment_value

return gains.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)

def calculate_net_amount_withdrawn(self):
"""
Calculo do montante para saque de um investimento fechado
(A tributar os ganhos).

Se tiver menos de um ano, a percentagem será de **22,5%**
(imposto = 45,00).

Se tiver entre um e dois anos, a percentagem será de **18,5%**
(imposto = 37,00).

Se tiver mais de dois anos, a percentagem será de **15%**
(imposto = 30,00).
"""
amount = self.calculate_amount_withdrawn()
gains = amount - self.investiment_value
initial_date = self.investiment_created_at.date()
withdrawn_date = self.withdrawn_created_at.date()

TAX_UNDER_12 = Decimal('0.225')
TAX_12_TO_24 = Decimal('0.185')
TAX_OVER_24 = Decimal('0.15')

months = self._full_months_between(initial_date, withdrawn_date)

if months < 12:
gains_tax = gains * TAX_UNDER_12
elif months > 24:
gains_tax = gains * TAX_OVER_24
else:
gains_tax = gains * TAX_12_TO_24

return (amount - gains_tax).quantize(
Decimal('0.01'), rounding=ROUND_HALF_EVEN
) # saldo montante(amount) limpo para retorno.

def calculate_net_gains_withdrawn(self) -> Decimal:
"""
Calculo dos Ganhos de um investimento fechado e tributado.
"""
amount = self.calculate_net_amount_withdrawn()
initial_investiment = self.investiment_value

return (amount - initial_investiment).quantize(
Decimal('0.01'), rounding=ROUND_HALF_EVEN
)

def _full_months_between(self, start, end):
"""
Calcula a quantidade de meses **inteiros** entre duas datas.

Um mês só é contabilizado se o período completar um ciclo mensal cheio,
isto é, o dia do mês em `end` deve ser maior ou = dia em `start`.

Regra de negócio:
- Meses parciais não são considerados.
- A contagem só avança quando o mês seguinte é completado.

exemplo: 12/09 -> 11/10 = 0 meses;
"""
months = (end.year - start.year) * 12 + (end.month - start.month)
if end.day < start.day:
months -= 1
return max(months, 0)
Empty file added code/business/tests/__init__.py
Empty file.
69 changes: 69 additions & 0 deletions code/business/tests/test_investiments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from decimal import Decimal
from django.utils import timezone
from datetime import datetime
from django.test import TestCase
from business.investiments import InvestimentBusiness


class InvestimentBusinessOpenTest(TestCase):
def setUp(self):
self.investiment = make_investiment(
value=Decimal('1000'),
created_at=timezone.make_aware(datetime(2025, 11, 13, 0, 0)),
withdrawn_at=timezone.make_aware(datetime(2025, 12, 12, 0, 0)),
)

def test_calculate_amount_is_equal(self):
self.assertEqual(
self.investiment.calculate_amount(), Decimal('1005.20')
)

def test_calculate_gains_is_equal(self):
self.assertEqual(self.investiment.calculate_gains(), Decimal('5.20'))


class InvestimentBusinessWithdrawnTest(TestCase):
def setUp(self):
self.investiment = make_investiment(
value=Decimal('1000'),
created_at=timezone.make_aware(datetime(2025, 11, 13, 0, 0)),
withdrawn_at=timezone.make_aware(datetime(2025, 12, 12, 0, 0)),
)

def test_calculate_amount_withdrawn_is_equal(self):
self.assertEqual(
self.investiment.calculate_amount_withdrawn(), Decimal('1000.00')
)

def test_calculate_gains_withdrawn_is_equal(self):
self.assertEqual(
self.investiment.calculate_gains_withdrawn(), Decimal('0')
)


class InvestimentBusinessNetWithdrawnTest(TestCase):
def setUp(self):
self.investiment = make_investiment(
value=Decimal('1000'),
created_at=timezone.make_aware(datetime(2025, 11, 13, 0, 0)),
withdrawn_at=timezone.make_aware(datetime(2025, 12, 12, 0, 0)),
)

def test_calculate_net_amount_withdrawn_is_equal(self):
self.assertEqual(
self.investiment.calculate_net_amount_withdrawn(), Decimal('1000')
)

def test_calculate_net_gains_withdrawn_is_equal(self):
self.assertEqual(
self.investiment.calculate_net_gains_withdrawn(), Decimal('0')
)


def make_investiment(*, value='1000.00', created_at, withdrawn_at=None):
"""Top-Level Construtora da classe de testes"""
return InvestimentBusiness(
investiment_value=Decimal(value),
investiment_created_at=created_at,
withdrawn_created_at=withdrawn_at,
)
Empty file added code/core/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions code/core/asgi.py
Original file line number Diff line number Diff line change
@@ -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/6.0/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()
Loading