Esse é um teste técnico para a vaga de Desenvolvedor Python Pleno.
Você recebe a tarefa de realizar a integração entre dois sistemas diferentes. Um desses sistemas é uma plataforma de CRM e o outro um sistema ERP. O sistema de CRM fornece APIs passivas e webhooks como ferramentas de integração enquanto o sistema ERP fornece apenas APIs passivas. O sistema CRM é hospedado em nuvem e você não possui acesso à infra dele, enquanto que o sistema ERP é hospedado on- premises em um banco de dados relacional e você possui acesso direto a essa infra. Não existe integração nativa entre os dois sistemas. Não existem impedimentos de infraestrutura que impeçam ou limitem essa integração. Suponha que você possua quaisquer tecnologias ou ferramentas disponíveis para concluir a tarefa. Baseado nesse cenário, responda as questões a seguir:
Resposta: Essa é de fato, uma tarefa desafiadora.
- Definiria os principais objetivos.
- Estudaria o comportamento da API.
- Escolheria as ferramentas mais adequadas para a tarefa.
- Planejaria a forma de comunicação entre os dois sistemas.
- Começaria a desenvolver a solução.
- Realizaria testes na solução criada.
- Se tudo estivesse funcionando corretamente, implementaria a solução.
- Pensaria em uma forma de monitorar continuamente a solução criada.
Resposta: Python, PostgreSQL, Redis, Celery, Pytest.
Python: Acredito que o Python seja uma boa escolha devido à familiaridade não apenas minha, mas também de todo o time com a tecnologia.
PostgreSQL: É um banco de dados sólido e confiável.
Redis: Acredito que um message broker seria útil nesse desafio.
Celery: Temos muito a ganhar ao usar o Celery neste projeto.
Pytest: É de suma importância testar nossa solução.
Resposta: CRM <-- Nossa Solução <-- ERP
d) Você conseguiria estimar quanto tempo levaria essa implementação? Considere uma jornada de trabalho padrão, de 40 horas semanais, de segunda a sexta.
Resposta: Como mencionei anteriormente, é uma tarefa muito desafiadora e depende muito do contexto do negócio, além do nível de experiência da equipe. Portanto, fazer uma estimativa precisa sem ter o contexto adequado seria incorreto.
Você possui dois dataframes, o dataframe A possui 1000 linhas e B possui 860 linhas. A seguir o mapa de colunas de cada dataframe:
Columns_A = [‘id’, ‘id_cliente’, ‘data_contratacao’, ‘id_contrato’, ‘valor_contrato’]
Columns_B = [‘id’, ‘id_cliente’, ‘data_cancelamento’, ‘id_contrato’, ‘valor_contrato’]
a) Escreva um código em Python que te permita encontrar uma lista de id_clientes dentro do dataframe A.
Resposta:
lista_clientes_1 = df_a['id_cliente'].unique()
display(lista_clientes_1)
array([101, 102, 103, 104, 105])
Resposta:
lista_clientes_2 = set(df_a['id_cliente'])
display(lista_clientes_2)
{101, 102, 103, 104, 105}
Set é um tipo de dado do python, uma coleção desordenada onde não pode haver números repetidos, então basicamente ao transformar uma lista de 'id_cliente' e um set ele elimina automaticamente as duplicatas, isso é mais eficiente em termos de memória, aqui está a definição de set segundo a documentação do python:
"5.4. Conjuntos
Python também inclui um tipo de dados para conjuntos, chamado set. Um conjunto é uma coleção desordenada de elementos, sem elementos repetidos. Usos comuns para conjuntos incluem a verificação eficiente da existência de objetos e a eliminação de itens duplicados. Conjuntos também suportam operações matemáticas como união, interseção,diferença e diferença simétrica.
Chaves ou a função set() podem ser usados para criar conjuntos." - Documentação do python
Resposta:
df_a_mais_b = df_a.merge(
right=df_b[['id_cliente', 'data_cancelamento']],
how='inner',
on='id_cliente'
)
display(df_a_mais_b)
id id_cliente data_contratacao id_contrato valor_contrato data_cancelamento
0 1 101 2023-01-15 201 1000.00 2023-05-15
1 2 102 2023-02-20 202 1500.50 2023-06-20
2 3 103 2023-03-10 203 800.25 2023-07-10
3 4 104 2023-04-05 204 1200.75 2023-08-05
4 5 105 2023-05-12 205 2000.50 2023-09-12
Resposta:
qtd_clientes_contratacao = (
df_a
.groupby(['data_contratacao'])
.agg({'id_cliente': 'count'})
.rename(columns={'id_cliente': 'clientes'})
.reset_index()
)
display(qtd_clientes_contratacao)
data_contratacao clientes
0 2023-01-15 1
1 2023-02-20 1
2 2023-03-10 1
3 2023-04-05 1
4 2023-05-12 1
Resposta:
qtd_clientes_cancelamento = (
df_b
.groupby(['data_cancelamento'])
.agg({'id_cliente': 'count'})
.rename(columns={'id_cliente': 'clientes'})
.reset_index()
)
display(qtd_clientes_cancelamento)
data_cancelamento clientes
0 2023-05-15 1
1 2023-06-20 1
2 2023-07-10 1
3 2023-08-05 1
4 2023-09-12 1
Você precisa desenvolver o back-end de um determinado sistema e a linguagem escolhida para resolver esse problema é Python, utilizando o framework Django. O sistema em questão é para agendamento de clientes que será utilizado por uma clínica odontológica. Junto dessa demanda, você também recebe a documentação funcional descrita a seguir:
- Endpoint para cadastrar novos agendamentos.
- Endpoint para editar ou cancelar um agendamento.
- Endpoint para listar todos os agendamentos.
- Endpoint para listar um agendamento específico.
- Validação para não permitir agendar dois clientes no mesmo horário.
- Cálculo de vagas disponíveis por data.
Baseando-se nessas considerações sobre o sistema, responda as questões que se seguem.
Resposta:
- Criaria o repositório do projeto.
- Clonaria o repositório para a minha máquina.
- Criaria um ambiente virtual.
- Instalaria as dependências necessárias.
- Criaria o projeto.
django-admin startproject core .
- Criaria o app.
python manage.py startapp agendamentos
- Adicionaria os apps agendamentos e rest_framework ao INSTALLED_APPS em core/settings.py.
- Criaria as migrações.
python manage.py makemigrations
- Em seguida, aplicaria as migrações.
python manage.py migrate
- Criaria os meus models.
- Criaria os serializers.
- Criaria as views.
- Definiria as rotas.
Resposta:
from rest_framework import status
from rest_framework import generics
from django.http import JsonResponse
from rest_framework import permissions
from agendamentos.models import Agendamentos
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from agendamentos.serializers import AgendamentosSerializer
from rest_framework.decorators import (
api_view,
permission_classes
)
class CadastrarAgendamento(generics.CreateAPIView):
queryset = Agendamentos.objects.all()
serializer_class = AgendamentosSerializer
permission_classes = [permissions.IsAuthenticated]
@api_view(http_method_names=['PATCH', 'DELETE'])
@permission_classes([permissions.IsAuthenticated])
def cancelar_e_editar(request, pk):
obj = get_object_or_404(klass=Agendamentos, id=pk)
if request.method == 'PATCH':
serializer = AgendamentosSerializer(
instance=obj,
data=request.data,
partial=True
)
if serializer.is_valid():
serializer.save()
return JsonResponse(
serializer.data,
status=status.HTTP_200_OK
)
return JsonResponse(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)
if request.method == 'DELETE':
obj.cancelamento = True
obj.save()
return Response(status=status.HTTP_204_NO_CONTENT)
class ListarAgendamentos(generics.ListCreateAPIView):
queryset = Agendamentos.objects.all()
serializer_class = AgendamentosSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
class AgendamentoEspecifico(generics.RetrieveAPIView):
queryset = Agendamentos.objects.all()
serializer_class = AgendamentosSerializer
permission_classes = [permissions.IsAuthenticated]
Resposta:
def validate_data(self, value):
if value < timezone.now():
raise serializers.ValidationError(
detail='O agendamento não pode ser feito no passado.',
code=status.HTTP_400_BAD_REQUEST
)
if value not in get_horario_disponiveis(value.date()):
raise serializers.ValidationError(
'Esse horário não está disponível.'
)
return value
Resposta: Você mencionou 'requisições', o que implica em mais de uma solicitação. Isso significa que uma delas será processada antes da outra. Assim que a primeira requisição tiver o seu agendamento registrado no banco de dados, a validação acima não permitirá outro agendamento no mesmo horário.
e) Apresente uma implementação do cálculo de vagas de agendamento. Suponha que cada atendimento demora 30 minutos para ser concluído e a clínica só funcione das 08:00 às 18:00, inclusive no horário do almoço. Desconsidere sábados e domingos.
Resposta:
from typing import Iterable
from agendamentos.models import Agendamentos
from datetime import (
date,
datetime,
timedelta,
timezone
)
def get_horario_disponiveis(data: date) -> Iterable[datetime]:
"""
Retorna uma lista com objetos do tipo datetime cujas datas são o mesmo dia
passado (data) e os horários são os horário disponíveis para aquele dia,
conforme outros agendamentos existem.
"""
start = datetime(
year=data.year,
month=data.month,
day=data.day,
hour=8,
minute=0,
tzinfo=timezone.utc
)
end = datetime(
year=data.year,
month=data.month,
day=data.day,
hour=18,
minute=0,
tzinfo=timezone.utc
)
delta = timedelta(minutes=30)
horario_disponiveis = set()
while start < end:
if not Agendamentos.objects.filter(data=start).exists():
horario_disponiveis.add(start)
start = start + delta
return horario_disponiveis
Uma pessoa do time financeiro encomenda para você um conjunto de relatórios financeiros que ela não consegue extrair a partir de bases de dados padrão do sistema utilizado na empresa. Você decide ir direto ao banco de dados para gerar esses relatórios e atender essa demanda. A tabela recuperada a partir do arquivo CSV que lhe foi entregue possui uma documentação incompleta, mostrada a seguir:
- Id: primary key
- Valor: valor original do título
- Status: status do título
- Baixa_data: data que o título foi baixado
- Id_renegociacao: codigo que vincula com o campo id_renegociacao_novo para renegociar um título
- Id_renegociacao_novo: codigo usado para gerar o título renegociado
Os demais campos não possuem qualquer documentação. A partir desta tabela responda às seguintes questões que foram trazidas pela pessoa do financeiro:
a) Qual é o valor total de títulos emitidos no mês de junho de 2020? Qual é o valor total de títulos com vencimento no mesmo período? Por que esses números são diferentes?
Resposta:
Valor total de títulos emitidos em junho (mês 6) de 2020:
select
sum(valor)
from base_de_dados
where (
year(data_emissao) = 2020
and month(data_emissao) = 6
)
+-----------------+
| sum(valor)|
+-----------------+
|540360.0000000052|
+-----------------+
Valor total de títulos vencimentos em junho (mês 6) de 2020:
select
sum(valor)
from base_de_dados
where (
year(data_vencimento) = 2020
and month(data_vencimento) = 6
)
+-----------------+
| sum(valor)|
+-----------------+
|3076802.819998371|
+-----------------+
Quantidade de títulos emitidos em junho (mês 6) de 2020:
select
count(data_emissao)
from base_de_dados
where (
year(data_emissao) = 2020
and month(data_emissao) = 6
)
+-------------------+
|count(data_emissao)|
+-------------------+
| 5423|
+-------------------+
Quantidade de títulos vencimento em junho (mês 6) de 2020:
select
count(data_vencimento)
from base_de_dados
where (
year(data_vencimento) = 2020
and month(data_vencimento) = 6
)
+----------------------+
|count(data_vencimento)|
+----------------------+
| 32082|
+----------------------+
O motivo é que havia mais vencimentos agendados para junho (mês 6) de 2020 do que a emissão de boletos em junho (mês 6) de 2020.
Resposta:
select
id,
data_emissao,
data_vencimento,
valor,
status,
pagamento_data,
baixa_data,
pagamento_valor
from base_de_dados
where (
year(pagamento_data) = 2020
and month(pagamento_data) = 7
)
+------+-------------------+-------------------+-----+------+-------------------+-------------------+---------------+
| id| data_emissao| data_vencimento|valor|status| pagamento_data| baixa_data|pagamento_valor|
+------+-------------------+-------------------+-----+------+-------------------+-------------------+---------------+
|110562|2019-06-05 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-22 00:00:00|2020-07-23 09:02:14| 110.06|
|112238|2019-06-06 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-31 00:00:00|2020-07-31 16:10:50| 99.9|
|119472|2019-06-11 00:00:00|2020-06-01 00:00:00| 79.9| R|2020-07-31 00:00:00|2020-07-31 11:37:50| 79.9|
|120832|2019-06-12 00:00:00|2020-06-01 00:00:00| 79.9| R|2020-07-01 00:00:00|2020-07-02 08:19:44| 82.1|
|131361|2019-06-24 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-02 00:00:00|2020-07-03 08:25:46| 102.52|
|131867|2019-06-24 00:00:00|2020-06-01 00:00:00| 79.9| R|2020-07-03 00:00:00|2020-07-06 08:30:09| 82.12|
|136426|2019-06-26 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-20 00:00:00|2020-07-21 08:10:25| 102.88|
|137384|2019-06-26 00:00:00|2020-06-01 00:00:00| 79.9| R|2020-07-03 00:00:00|2020-07-03 11:24:09| 81.55|
|151412|2019-07-08 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-06 00:00:00|2020-07-07 08:24:31| 102.6|
|153162|2019-07-09 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-09 00:00:00|2020-07-10 08:26:20| 102.66|
|161626|2019-07-16 00:00:00|2020-06-01 00:00:00| 79.9| R|2020-07-06 00:00:00|2020-07-07 08:24:32| 82.2|
|163660|2019-07-19 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-31 00:00:00|2020-08-01 08:17:15| 111.5|
|171636|2019-07-25 00:00:00|2020-06-01 00:00:00| 79.9| R|2020-07-16 00:00:00|2020-07-17 11:09:17| 87.34|
|173326|2019-07-29 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-01 00:00:00|2020-07-02 09:27:49| 106.7|
|175784|2019-07-30 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-08 00:00:00|2020-07-09 08:54:06| 107.82|
|176303|2019-07-30 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-03 00:00:00|2020-07-06 15:30:38| 107.02|
|183009|2019-08-06 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-23 00:00:00|2020-07-24 08:39:14| 110.2|
|183710|2019-08-06 00:00:00|2020-06-01 00:00:00| 69.9| R|2020-07-10 00:00:00|2020-07-13 09:14:10| 75.58|
|188279|2019-08-09 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-01 00:00:00|2020-07-02 09:28:02| 106.7|
|190243|2019-08-12 00:00:00|2020-06-01 00:00:00| 99.9| R|2020-07-03 00:00:00|2020-07-06 15:30:51| 107.02|
+------+-------------------+-------------------+-----+------+-------------------+-------------------+---------------+
Resposta:
select
count(id)
from base_de_dados
where (
year(data_vencimento) = 2020
and month(data_vencimento) = 7
and (
id_renegociacao is not null
or id_renegociacao_novo is not null
)
)
+---------+
|count(id)|
+---------+
| 1|
+---------+
Resposta:
Valor total de juros pagos por boletos em junho de 2020:
select
sum(valor) - sum(pagamento_valor) as juros_total
from base_de_dados
where (
year(data_vencimento) = 2020
and month(data_emissao) = 6
and status = 'R'
)
+------------------+
| juros_total|
+------------------+
|-7964.829999999201|
+------------------+
Juros pagos por boletos em junho de 2020:
select
valor - pagamento_valor as juros_total
from base_de_dados
where (
year(data_emissao) = 2020
and month(data_emissao) = 6
and status = 'R'
)
+--------------------+
| juros_total|
+--------------------+
| 0.0|
|-0.09999999999999432|
| 0.0|
| -4.409999999999997|
| -3.2299999999999898|
| 0.0|
| 0.0|
| 0.0|
| 0.0|
| -2.9099999999999966|
| -4.25|
| -7.429999999999993|
| 0.0|
| 0.0|
| 0.0|
| -3.1499999999999915|
| 0.0|
| 0.0|
| -4.709999999999994|
| 0.0|
+--------------------+
Resposta:
select
id,
id_renegociacao,
id_renegociacao_novo
from base_de_dados
where (
year(data_emissao) = 2020
and month(data_emissao) = 6
and (
id_renegociacao is not null
or id_renegociacao_novo is not null
)
)
+------+---------------+--------------------+
| id|id_renegociacao|id_renegociacao_novo|
+------+---------------+--------------------+
|400729| null| 541.0|
|407525| null| 542.0|
|407525| null| 542.0|
+------+---------------+--------------------+