Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guilherme Tonello - PR #5

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
credito_express
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.credito_express
__pycache__
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM tiangolo/uwsgi-nginx-flask:python3.6-alpine3.7
RUN apk --update --no-cache add bash nano python3-dev && pip3 install --upgrade pip
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
EXPOSE 3333
CMD python3 app
143 changes: 72 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,72 @@
## Sobre a Crédito Express

A Crédito Express é uma fintech voltada para servir instituições financeiras. Nosso objetivo é levar TAXAS ATRATIVAS para as pessoas, a partir do uso de tecnologia de ponta.

VENHA FAZER PARTE DESSA REVOLUÇÃO FINANCEIRA!


## Sobre o desafio

Com o aumento dos casos de COVID-19 muitos cidadãos tiveram aumentos nos gastos e redução nos ganhos, levando a eles endividarem-se em meios que as taxas de juros são extremamente altas. Neste desafio você deve construir uma aplicação para pessoas possam simular empréstimos com as melhores taxas do mercado financeiro.

A aplicação deve ter uma API para o cliente informar o seu CPF e numero de celular para se identificar e depois disso ele poderá efetuar simulações de empréstimo em outra API. Para simular é necessário informar o valor do empréstimo e o número de parcelas, sendo que podem ser 6, 12, 18, 24 ou 36.

Existe uma tabela de taxas, o calculo do empréstimo deve ser feito de acordo com ela. Existem 3 tipos diferentes de taxas: **negativado, score alto e score baixo**. Pessoas com score acima de 500 são consideradas com score alto nessa aplicação, pessoas sem cadastro na base recebem score 0.

Após obter o valor da taxa a ser aplicada para esse cliente, será necessário chamar uma API para fazer o cálculo da simulação. Os dados retornados pelo cálculo devem ser o retorno da sua API.


## Considerações

- O arquivo taxas.json possui uma coleção de taxas por característica. Os dados seguem o formato do exemplo abaixo, mas pode modificar a estrutura no seu projeto se precisar:

```javascript
{
"tipo": "NEGATIVADO",
"taxas": {"6": 0.04, "12": 0.045, "18": 0.05, "24": 0.053, "36": 0.055}
}
```
- O arquivo clientes.json possui uma coleção de objetos que representam os clientes que já tem pré-cadastro. Abaixo temos um exemplo do formato do objeto que também pode ter a estrutura modificada caso julgue necessário.

```javascript
{
"nome": "Roberto Filipe Figueiredo",
"cpf": "41882728564",
"celular": "6526332774",
"score": 300,
"negativado": false
}
```

- Abaixo temos um exemplo de um CURL para API de cálculo.

```shell
curl --request POST \
--url https://us-central1-creditoexpress-dev.cloudfunctions.net/teste-backend \
--header 'Content-Type: application/json' \
--data '{
"numeroParcelas": 12,
"valor": 10000,
"taxaJuros": 0.04
}'
```

### Pré-requisitos
- Desenvolvimento de API REST em Python;
- Utilização do MongoDB;
- Desenvolvimento de um Dockerfile/Docker-Compose.yml para rodar o projeto;
- Documentar como rodamos o projeto no README.MD;

### Diferenciais/Extras
- Implementação de Testes de unidade e/ou integração;
- Clean code;
- Segurança e resiliência;
- Utilização de padrões de projeto;
- Migrations e/ou seeders;
- Script para execução da aplicação;

## Pronto para começar o desafio?

- Faça um "fork" deste repositório na sua conta do Github;
- Após completar o desafio, crie um pull request nesse repositório comparando a sua branch com a master com o seu nome no título;
## Sobre a solução

Solução desenvolvida em python com Flask e mongodb. Aplicando conceitos SOLID e boas praticas de desenvolvimento para manter uma boa estrutura e um codigo limpo.
Além disso também aplicado alguns conceitos como Decorators, upload de arquivos, docker e ORM.

## Como Rodar

Para rodar e subir a aplicação execute o seguinte comando na pasta raiz do projeto:
```shell
docker-compose up -d
```
Desta forma, estará rodando os container responsáveis pela API e pelo banco de dados da aplicação.
Obs: o banco de dados estará vazio e precisará ser preenchido.

# Utilizando
## Preenchendo o banco
- Primeiramente é necessario preencher o banco de dados por meio dos arquivos fornecidos na pasta dados.
Para isso há duas rotas, uma para as taxas e outra para os usuarios.
### Usuarios
- Rota: [POST] http://0.0.0.0:3333/user/insert-file
- Inserir como corpo da requisição o arquivo "clientes.json" com o nome da key como "file"

### Taxas
- Rota: [POST] http://0.0.0.0:3333/taxa/insert-file
- Inserir como corpo da requisição o arquivo "taxas.json" com o nome da key como "file"

## Plano B
- Caso não obtenha sucesso em preencher o banco com os arquivos, também é possivel preenche-los informando o array de JSON manualmente no BODY da request. Da seguinte forma:
### Usuarios
- Rota: [POST] http://0.0.0.0:3333/user/insert
- Inserir como corpo da requisição o conteudo do arquivo "clientes.json".

### Taxas
- Rota: [POST] http://0.0.0.0:3333/taxa/insert
- Inserir como corpo da requisição o conteudo do arquivo "taxas.json".

## Rotas
- Algumas rotas possivéis da API

### Login
- Rota: [GET] http://0.0.0.0:3333/user/login
- Corpo: Informar no corpo da requisição um JSON contendo o **cpf** e o **celular** de algum usuario cadastrado anteriormente. Ex:
```javascript
{
"cpf":"42935087160",
"celular":"41996268329"
}
```
- Retorno: Retornará um Token JWT possibilitando a simulação de emprestimo.
- **Obs:** Caso não haja usuario cadastrado com esses dados será retornado Um Token JWT correspondente a um Usuario Visitante.

### Simular Emprestimo
- Rota: [GET] http://0.0.0.0:3333/simulacao_emprestimo/simular
- Header: Deve conter um Header **Authorization** contendo o Token JWT retornado na request de Login.
- Corpo: Informar no corpo da requisição um JSON contendo o valores de **valor** e o **numeroParcelas** que serão usados pra simular o empréstimo. Ex:
```javascript
{
"valor": 10000,
"numeroParcelas": 12
}
```
- Retorno: Retornará um JSON contendo o resultado da simulaçao do Emprestimo. Ex:
```javascript
{
"numeroParcelas": 12,
"outrasTaxas": 85,
"total": 10335.0,
"valorJuros": 250.0,
"valorParcela": 861.25,
"valorSolicitado": 10000
}
```
3 changes: 3 additions & 0 deletions app/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.pythonPath": "c:\\Users\\Guilherme\\Documents\\Projects\\flask\\api_credito_express\\credito_express\\Scripts\\python.exe"
}
20 changes: 20 additions & 0 deletions app/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import logging
logging.getLogger().setLevel( logging.INFO )

from flask import Flask

def init_app( app ):
#Database
from repository import init_db
init_db()

#Rotas
from routes import init_routes
init_routes( app )

app.run( host = "0.0.0.0", port = 3333 )


if __name__ == '__main__':
app = Flask(__name__)
init_app( app )
1 change: 1 addition & 0 deletions app/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AUTH_HEADER_NAME = 'Authorization'
43 changes: 43 additions & 0 deletions app/auth/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import jwt
import traceback

from time import time
from flask import request

from auth import AUTH_HEADER_NAME

SECRET_KEY = 'chave-secreta-do-usuario'
ALGORITHM = 'HS256'

def generate_user_token( func ):
def a( *args, **kwargs ):
user_id = func( *args, **kwargs )

if type(user_id) == tuple:
return user_id

payload = {
'uid': user_id,
'exp': int(time()) + 3600
}

return "Bearer {}".format( jwt.encode( payload, SECRET_KEY, algorithm = ALGORITHM ).decode( 'utf-8' ) )

return a

def validate_user_token_and_get_id( func ):
def a( *args, **kwargs ):
try:
user_token = request.headers.get( AUTH_HEADER_NAME )
user_token = user_token.split()[-1]
user_id = jwt.decode( user_token, SECRET_KEY, algorithms = [ ALGORITHM ] )

user_id = user_id[ 'uid' ]

return func( user_id, *args, **kwargs )

except Exception as e:
traceback.print_exc()
return "Authentication fail", 500
return a

Empty file added app/controller/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions app/controller/simulacao_emprestimo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import traceback

from models.emprestimo import Emprestimo, NUM_PARCELAS, VAL_EMPRESTIMO
from service import user as user_service, tipo_taxa as tipo_taxa_service, taxa as taxa_service, emprestimo as emprestimo_service

def simular_emprestimo( user_id, emprestimo ):
try:
emprestimo = Emprestimo( user_id, emprestimo[ VAL_EMPRESTIMO ], emprestimo[ NUM_PARCELAS ] )

user_tipo_taxa = user_service.get_tipo_taxa_user_by_user_id( user_id )
taxa_juros = taxa_service.get_val_juros_by_tipo_taxa_and_parcelas( user_tipo_taxa, emprestimo.num_parcelas )

response = emprestimo_service.calular_emprestimo_request( emprestimo, taxa_juros )

return response
except Exception as e:
traceback.print_exc()

return "Error ao simular emprestimo", 505
29 changes: 29 additions & 0 deletions app/controller/taxa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import traceback, logging

from service import taxa as taxa_service, tipo_taxa as tipo_taxa_service

from models.tipo_taxa import JSON_TIPO, JSON_TAXAS
from models.taxa import ID_TIPO_TAXA

def insert_taxas( taxas ):
try:
for taxa in taxas:
tipo_taxa = taxa[ JSON_TIPO ]

id_tipo_taxa = tipo_taxa_service.find_tipo_taxa( tipo_taxa )
if id_tipo_taxa is None:
id_tipo_taxa = tipo_taxa_service.insert_tipo_taxa( tipo_taxa ).id
else:
id_tipo_taxa = id_tipo_taxa.id

taxas_list = taxa_service.build_taxas_list( taxa[ JSON_TAXAS ], id_tipo_taxa )

for taxa in taxas_list:
taxa_service.insert_taxa( taxa )

except Exception as e:
logging.error( traceback.format_exc() )
return "Falha ao inserir taxas", 500

else:
return "Inserção de taxas efetuada com sucesso", 200
29 changes: 29 additions & 0 deletions app/controller/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json, logging

from service import user as user_service

def insert_users( users ):
response = None
try:
response = user_service.insert_users( users )

except Exception as e:
logging.error( e )
return "Falha ao inserir usuarios", 500

else:
return json.dumps( response )

def get_user_id_by_cpf_and_cellphone( user ):
try:
user = user_service.find_user_by_cpf_and_cellphone( user['cpf'], user['celular'] )
if user:
return str( user.id )
else:
logging.info( "Usuario visitante" )
return 0

except Exception as e:
logging.error( e )
return "Falha ao buscar usuario", 500

4 changes: 4 additions & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .user import User
from .taxa import Taxa
from .tipo_taxa import Tipo_taxa
from .emprestimo import Emprestimo
13 changes: 13 additions & 0 deletions app/models/emprestimo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Emprestimo():
id_usuario = int()
val_emprestimo = float()
numParcelas = int()

def __init__( self, id_usuario, val_emprestimo, num_parcelas ):
self.id_usuario = id_usuario
self.val_emprestimo = val_emprestimo
self.num_parcelas = num_parcelas

#ATRIBUTOS JSON
NUM_PARCELAS = 'numeroParcelas'
VAL_EMPRESTIMO = 'valor'
12 changes: 12 additions & 0 deletions app/models/taxa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from mongoengine import Document, IntField, FloatField, ReferenceField, CASCADE

from .tipo_taxa import Tipo_taxa

class Taxa( Document ):
num_parcelas = IntField( required = True, max_value = 36, min_value = 6 )
juros = FloatField( required = True, max_value = 1, min_value = 0 )
id_tipo_taxa = ReferenceField( Tipo_taxa, reverse_delete_rule = CASCADE )

NUM_PARCELAS = 'num_parcelas'
JUROS = 'juros'
ID_TIPO_TAXA = 'id_tipo_taxa'
15 changes: 15 additions & 0 deletions app/models/tipo_taxa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from mongoengine import Document, StringField

class Tipo_taxa( Document ):
descr = StringField( required = True, unique = True )

#ATRIBUTOS JSON
JSON_TIPO = "tipo"
JSON_TAXAS = "taxas"

#TIPOS TAXA
class TipoTaxaEnum():
NEGATIVADO = 'NEGATIVADO'
SCORE_ALTO = 'SCORE_ALTO'
SCORE_BAIXO = 'SCORE_BAIXO'
VALOR_SCORE_ALTO = 500
30 changes: 30 additions & 0 deletions app/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from mongoengine import Document, StringField, BooleanField, IntField

class User( Document ):
nome = StringField( required = True )
cpf = StringField( required = True, max_length = 11 )
celular = StringField( required = True, max_length = 11 )
score = IntField( required = True )
negativado = BooleanField( required = True, default = False)

def __str__( self ):
return """Nome: {nome},
CPF: {cpf},
Celular: {celular},
Score: {score},
Negativado: {negativado}
""".format( nome=self.nome,
cpf=self.cpf,
celular=self.celular,
score=self.score,
negativado=self.negativado )

#Atributos
NOME = 'nome'
CPF = 'cpf'
CELULAR = 'celular'
SCORE = 'score'
NEGATIVADO = 'negativado'

#ENUM TIPO USUARIO
VISITANTE = 0
Loading