Skip to content

lfsc09/challenge-BeTalent

Repository files navigation

challenge-BeTalent

Sobre

Este teste prático foi desenvolvido com:

  • Adonis.s como framework para a API Rest.
  • Lucid como plugin ORM (interface com o DB e Migrations) disponível pelo próprio Adonis.js.
  • VinJS como plugin para validação disponível pelo próprio Adonis.js.
  • Japa como plugin de testes disponível pelo próprio Adonis.js.
  • Mysql como banco de dados.
  • Docker para geração da infra (DB e Gateways usadas).

Rotas

Rota Método Tipo Descrição
/login POST pública Realizar o login (Para rotas administrativas)
/purchase POST pública Realizar uma compra de um cliente informando produtos
/gateways/:id/active PUT privada Ativar/desativar um gateway
/gateways/:id/priority PUT privada Alterar a prioridade de um gateway
/users GET privada Listar todos os usuários
/users POST privada Criar um usuário
/users/:id PUT privada Editar um usuário
/users/:id DELETE privada Apagar um usuário
/products GET privada Listar todos os produtos
/products POST privada Criar um produto
/products/:id PUT privada Editar um produto
/products/:id DELETE privada Apagar um produto
/clients GET privada Listar todos os clientes
/clients/:id GET privada Detalhes do cliente e todas suas compras
/purchases GET privada Listar todas as compras
/purchases/:id GET privada Detalhes de uma compra
/reimburse POST privada Realizar reembolso de uma compra junto ao gateway com validação por roles

Roadmap

  • Criar docker compose configurando as Gateways e o DB.
  • Implementar Controller, Validações, Models, Migration e Testes de usuários.
  • Implementar Controller, Validações, Models, Migration e Testes de produtos.
  • Implementar Controller, Models, Migration e Testes de clientes.
  • Implementar Controller, Models, Migration e Testes de gateways.
  • Implementar Controller, Models, Migration e Testes de transações.
  • Gerar middleware the autenticação.
  • Gerar middleware de autorização para as roles.

Rodando o projeto

Use o HOST 127.0.0.1 se estiver usando WSL2.

Testes

1. Iniciar os Mocks das Gateways e Banco de Dados

Container das Gateways não é utilizado nos testes, a conexão com elas é substituída.

docker compose up --build --detach
docker compose down

2. Rodar a Migration & Seeds (Manualmente)

Para os testes as seeds não são necessárias.

npm run migrations:fresh

3. Rodar testes

npm run test

Dev

1. Iniciar os Mocks das Gateways e do Banco de Dados

Gateway 1: http://localhost:3001 Gateway 2: http://localhost:3002 Banco de Dados: http://localhost:3306

docker compose up --build --detach
docker compose down

2. Rodar a Migration & Seeds (Manualmente)

npm run migrations:fresh

3. Rodar a API (Sem buildar)

Rode as Migrations antes.

O projeto rodará em http://localhost:3333

npm run dev

Prod

Rodar o projeto fazendo build.

O projeto rodará em http://localhost:8080 Gateway 1: http://localhost:3001 Gateway 2: http://localhost:3002 Banco de Dados: http://localhost:3306

docker compose --profile production up --build --detach
docker compose --profile production down

Rotas detalhes

purchase

HTTP Request
Endpoint: /purchase
Method: POST

Response Codes:
 - 201: Sucesso
 - 404: Cliente ou Produto não encontrado
 - 422: Payload passada inválida
Request Payload (Exemplo)
{
  "clientName": "Cliente",
  "clientEmail": "[email protected]",
  "products": [
    {
      "productId": "26256e4e-3dc9-4e1d-983e-7b059d1ae0b4",
      "quantity": 2
    },
    {
      "productId": "39ff6d1b-37da-4e05-9828-3ebd4b988336",
      "quantity": 1
    }
  ],
  "cardNumbers": "5569000000006063",
  "cardCvv": "010"
}
Implementação

A implementação desta rota é dividida em 2 partes (serviços).

Na primeira, de forma sincrona com a request, o serviço create_purchase.ts captura/cria o Cliente, captura os produtos na request, gera uma entidade Transaction que computa o valor total da compra, persiste essa entidade no banco e por fim invoca por meio de um Emitter do Adonis a segunda etapa.

Optei por passar os dados do cartão na request e também por salvá-los encriptados na tabela transaction, para possibilitar que o serviço process_payment.ts pudesse ser totalmente independente.

O fim da primeira etapa ja retorna uma resposta com um dos status acima, de forma a prender os clients apenas para validação dos dados da request.

Na segunda, de forma assincrona, o serviço process_payment.ts executa. Ele restaura os dados de uma transação (no caso a que veio da primeira etapa), altera o status da transação conforme o progresso, escolhe uma das gateways implementadas (usando a prioridade) e conecta a ela fazendo o pagamento com os dados necessários.

Caso a gateway esteja indisponível haverá no máximo 3 retries até a gateway ser marcada como indisponível. Os retries são inalteráveis e seguem um backoff linear. A indisponibilidade da Gateway pode ser automaticamente revertida por tempo configurado pelas variáveis de ambiente AUTO_RECOVER_GATEWAY_IN_MINUTES=2 e AUTO_RECOVER_GATEWAY=true. (Por padrão as gateways se auto recuperam)

A escolha das gateways são através da factory payment_factory.ts, que le do DB as gateways cadastradas, e baseada na escolhida retorna uma instacia da implementação da gateway. Dessa forma adicionar novas gateways requer apenas criar uma nova implemetação do contrato payment_gateway.ts, adicionar essa gateway ao DB e adicionar novas variáveis de ambiente para o HOST e PORT dela.

O serviço process_payment.ts pode ser chamado a qualquer momento, com a unica restrição de ser passado o id da transação que irá ser processado.


reimburse

HTTP Request
Endpoint: /reimburse/:id
Method: POST

Response Codes:
 - 200: Sucesso
 - 404: Transação não encontrada
Implementação

Da mesma forma que na compra, o reembolso também é dividido em 2 serviços.

No primeiro, de forma sincrona com a request, o serviço reimburse_purchase.ts captura do ID da compra e verifica se é existente apenas para retornar 404 caso não exista.

O fim do primeiro serviço retorna ou 200 se a compra existe ou 404 se não existe.

No segundo serviço process_reimbursement.ts, é restaurado os dados da transação, escolhido a gateway especifica em que a compra foi realizada e conecta a ela fazendo o reembolso com o externalID da transação.

Caso a gateway esteja indisponivel haverá tambem no máximo 3 retries até a gateway ser marcada como indisponível. Os retries são inalteráveis e seguem um backoff linear. A indisponibilidade da Gateway pode ser automaticamente revertida por tempo configurado pelas variáveis de ambiente AUTO_RECOVER_GATEWAY_IN_MINUTES=2 e AUTO_RECOVER_GATEWAY=true. (Por padrão as gateways se auto recuperam)

O serviço process_reimbursement.ts pode ser chamado a qualquer momento, com a unica restrição de ser passado o id da transação que irá ser processado.


gateways active edit

HTTP Request
Endpoint: /gateways/:id/active
Method: PUT

Response Codes:
 - 200: Sucesso
 - 404: Gateway não encontrado
 - 422: Payload passada inválida
Request Payload (Exemplo)
{
  "isActive": true
}

gateways priority edit

HTTP Request
Endpoint: /gateways/:id/priority
Method: PUT

Response Codes:
 - 200: Sucesso
 - 404: Gateway não encontrado
 - 422: Payload passada inválida
Request Payload (Exemplo)
{
  "priority": 4
}

users list

HTTP Request
Endpoint: /users
Method: GET

Response Codes:
 - 200: Sucesso
Response Payload (Exemplo)
[
  {
    "id": "ae17635e-b683-4e35-aaae-396e2c29d723",
    "email": "[email protected]",
    "role": "ADMIN",
    "createdAt": "2025-03-06T06:17:19.000+00:00",
    "updatedAt": "2025-03-06T06:17:19.000+00:00"
  }
]

users new

HTTP Request
Endpoint: /users
Method: POST

Response Codes:
 - 201: Sucesso
 - 422: Payload passada inválida
Request Payload (Exemplo)
{
  "email": "[email protected]",
  "password": "12345678",
  "role": "ADMIN"
}

users edit

HTTP Request
Endpoint: /users/:id
Method: PUT

Response Codes:
 - 200: Sucesso
 - 404: Usuário não encontrado
 - 422: Payload passada inválida
Request Payload (Exemplo)
{
  "email": "[email protected]", // Opcional
  "password": "12345678", // Opcional
  "role": "ADMIN" // Opcional
}

users delete

HTTP Request
Endpoint: /users/:id
Method: DELETE

Response Codes:
 - 200: Sucesso
 - 404: Usuário não encontrado

products list

HTTP Request
Endpoint: /products
Method: GET

Response Codes:
 - 200: Sucesso
Response Payload (Exemplo)
[
  {
    "id": "ae17635e-b683-4e35-aaae-396e2c29d723",
    "name": "produto 1",
    "amount": 10.5,
    "createdAt": "2025-03-06T06:17:19.000+00:00",
    "updatedAt": "2025-03-06T06:17:19.000+00:00"
  }
]

products new

HTTP Request
Endpoint: /products
Method: POST

Response Codes:
 - 201: Sucesso
 - 422: Payload passada inválida
Request Payload (Exemplo)
{
  "name": "produto 1",
  "amount": 40.25
}

products edit

HTTP Request
Endpoint: /products/:id
Method: PUT

Response Codes:
 - 200: Sucesso
 - 404: Produto não encontrado
 - 422: Payload passada inválida
Request Payload (Exemplo)
{
  "name": "produto 2", // Opcional
  "amount": 27.78 // Opcional
}

products delete

HTTP Request
Endpoint: /products/:id
Method: DELETE

Response Codes:
 - 200: Sucesso
 - 404: Produto não encontrado

clients list all

HTTP Request
Endpoint: /clients
Method: GET

Response Codes:
 - 200: Sucesso
Response Payload (Exemplo)
[
  {
    "id": "ae17635e-b683-4e35-aaae-396e2c29d723",
    "name": "Client",
    "email": "[email protected]",
    "createdAt": "2025-03-06T06:17:19.000+00:00",
    "updatedAt": "2025-03-06T06:17:19.000+00:00"
  }
]

client details

HTTP Request
Endpoint: /clients/:id
Method: GET

Response Codes:
 - 200: Sucesso
 - 404: Cliente não encontrado
Response Payload (Exemplo)
[
  {
    "id": "ae17635e-b683-4e35-aaae-396e2c29d723",
    "name": "Cliente",
    "email": "[email protected]",
    "purchases": [
      {
        "id": "ae17635e-b683-4e35-aaae-396e2c29d723",
        "status": "created",
        "amount": 10.5,
        "createdAt": "2025-03-06T06:17:19.000+00:00",
        "updatedAt": "2025-03-06T06:17:19.000+00:00"
      }
    ]
  }
]

purchases list all

HTTP Request
Endpoint: /purchases
Method: GET

Response Codes:
 - 200: Sucesso
Response Payload (Exemplo)
[
  {
    "id": "ae17635e-b683-4e35-aaae-396e2c29d723",
    "status": "created",
    "amount": 10.5,
    "createdAt": "2025-03-06T06:17:19.000+00:00",
    "updatedAt": "2025-03-06T06:17:19.000+00:00"
  }
]

purchase details

HTTP Request
Endpoint: /purchases/:id
Method: GET

Response Codes:
 - 200: Sucesso
 - 404: Compra não encontrada
Response Payload (Exemplo)
[
  {
    "id": "ae17635e-b683-4e35-aaae-396e2c29d723",
    "clientName": "Cliente",
    "clientEmail": "[email protected]",
    "gatewayName": "Gateway 1",
    "externalId": "ae17635e-b683-4e35-aaae-122e2c29d723",
    "status": "approved",
    "amount": 10.5,
    "products": [
      {
        "name": "Produto 1",
        "quantity": 5
      }
    ],
    "createdAt": "2025-03-06T06:17:19.000+00:00",
    "updatedAt": "2025-03-06T06:17:19.000+00:00"
  }
]

Dificuldades

  • Decidir se nos testes dos endpoints usava um Faker em memória dos serviços *_database.ts, ou se dava hit no database. Como o Model do adonis lida automaticamente retornando 404 nos findOrFail usar o serviço com database ficava mais simples nos testes. Porém usando o database requeri que o docker compose seja rodado antes de executar testes, consumindo mais recursos.

About

BeTalent challenge

Topics

Resources

License

Stars

Watchers

Forks

Languages