An open API service indexing awesome lists of open source software.

https://github.com/lfsc09/challenge-betalent


https://github.com/lfsc09/challenge-betalent

adonisjs docker lucid-orm mysql typescript vinejs

Last synced: about 2 months ago
JSON representation

Awesome Lists containing this project

README

          

# challenge-BeTalent

## Sobre

Este teste prático foi desenvolvido com:

- [`Adonis.s`](https://docs.adonisjs.com/guides/preface/introduction) como framework para a API Rest.
- [`Lucid`](https://lucid.adonisjs.com/docs/introduction) como plugin ORM (interface com o DB e Migrations) disponível pelo próprio `Adonis.js`.
- [`VinJS`](https://vinejs.dev/docs/introduction) como plugin para validação disponível pelo próprio `Adonis.js`.
- [`Japa`](https://japa.dev/docs/introduction) 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`](#purchase) | `POST` | pública | Realizar uma compra de um cliente informando produtos |
| [`/gateways/:id/active`](#gateways-active-edit) | `PUT` | privada | Ativar/desativar um gateway |
| [`/gateways/:id/priority`](#gateways-priority-edit) | `PUT` | privada | Alterar a prioridade de um gateway |
| [`/users`](#users-list) | `GET` | privada | Listar todos os usuários |
| [`/users`](#users-new) | `POST` | privada | Criar um usuário |
| [`/users/:id`](#users-edit) | `PUT` | privada | Editar um usuário |
| [`/users/:id`](#users-delete) | `DELETE` | privada | Apagar um usuário |
| [`/products`](#products-list) | `GET` | privada | Listar todos os produtos |
| [`/products`](#products-new) | `POST` | privada | Criar um produto |
| [`/products/:id`](#products-edit) | `PUT` | privada | Editar um produto |
| [`/products/:id`](#products-delete) | `DELETE` | privada | Apagar um produto |
| [`/clients`](#clients-list-all) | `GET` | privada | Listar todos os clientes |
| [`/clients/:id`](#client-details) | `GET` | privada | Detalhes do cliente e todas suas compras |
| [`/purchases`](#purchases-list-all) | `GET` | privada | Listar todas as compras |
| [`/purchases/:id`](#purchase-details) | `GET` | privada | Detalhes de uma compra |
| [`/reimburse`](#reimburse) | `POST` | privada | Realizar reembolso de uma compra junto ao gateway com validação por roles |

## Roadmap

- [x] Criar docker compose configurando as Gateways e o DB.
- [x] Implementar Controller, Validações, Models, Migration e Testes de usuários.
- [x] Implementar Controller, Validações, Models, Migration e Testes de produtos.
- [x] Implementar Controller, Models, Migration e Testes de clientes.
- [x] Implementar Controller, Models, Migration e Testes de gateways.
- [x] Implementar Controller, Models, Migration e Testes de transações.
- [x] Gerar middleware the autenticação.
- [x] 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._

```bash
docker compose up --build --detach
```

```bash
docker compose down
```

#### 2. Rodar a Migration & Seeds (Manualmente)

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

```bash
npm run migrations:fresh
```

#### 3. Rodar testes

```bash
npm run test
```

### Dev

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

Gateway 1: [http://localhost:3001](http://localhost:3001)
Gateway 2: [http://localhost:3002](http://localhost:3002)
Banco de Dados: [http://localhost:3306](http://localhost:3306)

```bash
docker compose up --build --detach
```

```bash
docker compose down
```

#### 2. Rodar a Migration & Seeds (Manualmente)

```bash
npm run migrations:fresh
```

#### 3. Rodar a API (Sem buildar)

_Rode as Migrations antes._

O projeto rodará em [http://localhost:3333](http://localhost:3333)

```bash
npm run dev
```

### Prod

Rodar o projeto fazendo build.

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

```bash
docker compose --profile production up --build --detach
```

```bash
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)

```json
{
"clientName": "Cliente",
"clientEmail": "cliente@betalent.com",
"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)

```json
{
"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)

```json
{
"priority": 4
}
```

### users list

##### HTTP Request

Endpoint: /users
Method: GET

Response Codes:
- 200: Sucesso

##### Response Payload (Exemplo)

```json
[
{
"id": "ae17635e-b683-4e35-aaae-396e2c29d723",
"email": "admin@betalent.com",
"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)

```json
{
"email": "admin@betalent.com",
"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)

```json
{
"email": "admin@betalent.com", // 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)

```json
[
{
"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)

```json
{
"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)

```json
{
"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)

```json
[
{
"id": "ae17635e-b683-4e35-aaae-396e2c29d723",
"name": "Client",
"email": "client@adonis.com",
"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)

```json
[
{
"id": "ae17635e-b683-4e35-aaae-396e2c29d723",
"name": "Cliente",
"email": "client@adonis.com",
"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)

```json
[
{
"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)

```json
[
{
"id": "ae17635e-b683-4e35-aaae-396e2c29d723",
"clientName": "Cliente",
"clientEmail": "client@adonis.com",
"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.