Este teste prático foi desenvolvido com:
Adonis.scomo framework para a API Rest.Lucidcomo plugin ORM (interface com o DB e Migrations) disponível pelo próprioAdonis.js.VinJScomo plugin para validação disponível pelo próprioAdonis.js.Japacomo plugin de testes disponível pelo próprioAdonis.js.Mysqlcomo banco de dados.Dockerpara geração da infra (DB e Gateways usadas).
| 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 |
- 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.
Use o HOST 127.0.0.1 se estiver usando WSL2.
Container das Gateways não é utilizado nos testes, a conexão com elas é substituída.
docker compose up --build --detachdocker compose downPara os testes as seeds não são necessárias.
npm run migrations:freshnpm run testGateway 1: http://localhost:3001 Gateway 2: http://localhost:3002 Banco de Dados: http://localhost:3306
docker compose up --build --detachdocker compose downnpm run migrations:freshRode as Migrations antes.
O projeto rodará em http://localhost:3333
npm run devRodar 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 --detachdocker compose --profile production downEndpoint: /purchase
Method: POST
Response Codes:
- 201: Sucesso
- 404: Cliente ou Produto não encontrado
- 422: Payload passada inválida
{
"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"
}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.
Endpoint: /reimburse/:id
Method: POST
Response Codes:
- 200: Sucesso
- 404: Transação não encontrada
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.
Endpoint: /gateways/:id/active
Method: PUT
Response Codes:
- 200: Sucesso
- 404: Gateway não encontrado
- 422: Payload passada inválida
{
"isActive": true
}Endpoint: /gateways/:id/priority
Method: PUT
Response Codes:
- 200: Sucesso
- 404: Gateway não encontrado
- 422: Payload passada inválida
{
"priority": 4
}Endpoint: /users
Method: GET
Response Codes:
- 200: Sucesso
[
{
"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"
}
]Endpoint: /users
Method: POST
Response Codes:
- 201: Sucesso
- 422: Payload passada inválida
{
"email": "[email protected]",
"password": "12345678",
"role": "ADMIN"
}Endpoint: /users/:id
Method: PUT
Response Codes:
- 200: Sucesso
- 404: Usuário não encontrado
- 422: Payload passada inválida
{
"email": "[email protected]", // Opcional
"password": "12345678", // Opcional
"role": "ADMIN" // Opcional
}Endpoint: /users/:id
Method: DELETE
Response Codes:
- 200: Sucesso
- 404: Usuário não encontrado
Endpoint: /products
Method: GET
Response Codes:
- 200: Sucesso
[
{
"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"
}
]Endpoint: /products
Method: POST
Response Codes:
- 201: Sucesso
- 422: Payload passada inválida
{
"name": "produto 1",
"amount": 40.25
}Endpoint: /products/:id
Method: PUT
Response Codes:
- 200: Sucesso
- 404: Produto não encontrado
- 422: Payload passada inválida
{
"name": "produto 2", // Opcional
"amount": 27.78 // Opcional
}Endpoint: /products/:id
Method: DELETE
Response Codes:
- 200: Sucesso
- 404: Produto não encontrado
Endpoint: /clients
Method: GET
Response Codes:
- 200: Sucesso
[
{
"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"
}
]Endpoint: /clients/:id
Method: GET
Response Codes:
- 200: Sucesso
- 404: Cliente não encontrado
[
{
"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"
}
]
}
]Endpoint: /purchases
Method: GET
Response Codes:
- 200: Sucesso
[
{
"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"
}
]Endpoint: /purchases/:id
Method: GET
Response Codes:
- 200: Sucesso
- 404: Compra não encontrada
[
{
"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"
}
]- 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 doadonislida automaticamente retornando404nosfindOrFailusar o serviço com database ficava mais simples nos testes. Porém usando o database requeri que odocker composeseja rodado antes de executar testes, consumindo mais recursos.