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óprioAdonis.js
.VinJS
como plugin para validação disponível pelo próprioAdonis.js
.Japa
como plugin de testes disponível pelo próprioAdonis.js
.Mysql
como banco de dados.Docker
para 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 --detach
docker compose down
Para os testes as seeds não são necessárias.
npm run migrations:fresh
npm run test
Gateway 1: http://localhost:3001 Gateway 2: http://localhost:3002 Banco de Dados: http://localhost:3306
docker compose up --build --detach
docker compose down
npm run migrations:fresh
Rode as Migrations antes.
O projeto rodará em http://localhost:3333
npm run dev
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
Endpoint: /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 doadonis
lida automaticamente retornando404
nosfindOrFail
usar o serviço com database ficava mais simples nos testes. Porém usando o database requeri que odocker compose
seja rodado antes de executar testes, consumindo mais recursos.