diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..05a8d8714
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+node_modules
+npm-debug.log
+.DS_Store
+.git
+.gitignore
+.env
+notes.txt
+mysql_data
+sql
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..ee4d806d7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+package-lock.json
+notes.txt
+.env
+mysql_data
\ No newline at end of file
diff --git a/Dockerfile.backend b/Dockerfile.backend
new file mode 100644
index 000000000..e529147f1
--- /dev/null
+++ b/Dockerfile.backend
@@ -0,0 +1,7 @@
+FROM node:18
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+EXPOSE 3000
+CMD ["npm", "start"]
\ No newline at end of file
diff --git a/Dockerfile.worker b/Dockerfile.worker
new file mode 100644
index 000000000..177f37c65
--- /dev/null
+++ b/Dockerfile.worker
@@ -0,0 +1,7 @@
+FROM node:18
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY models/ models/
+COPY workers/ workers/
+CMD ["node", "workers/currencys.js"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 22af01577..93091d2d6 100644
--- a/README.md
+++ b/README.md
@@ -1,82 +1,106 @@
#
Bravo Challenge
-[[English](README.md) | [Portuguese](README.pt.md)]
+Para esse desafio foi usada a seguinte arquitetura:
-Build an API, which responds to JSON, for currency conversion. It must have a backing currency (USD) and make conversions between different currencies with **real and live values**.
+
+
+
-The API must convert between the following currencies:
+## Diagramas de sequência
-- USD
-- BRL
-- EUR
-- BTC
-- ETH
+### Worker
-Other coins could be added as usage.
+
+
+
-Ex: USD to BRL, USD to BTC, ETH to BRL, etc...
+### API
-The request must receive as parameters: The source currency, the amount to be converted and the final currency.
+
+
+
-Ex: `?from=BTC&to=EUR&amount=123.45`
+## Containers (docker):
-Also build an endpoint to add and remove API supported currencies using HTTP verbs.
+### db
-The API must support conversion between FIAT, crypto and fictitious. Example: BRL->HURB, HURB->ETH
+Banco de dados MySQL responsável por persistir os dados em um estado sólido.
-"Currency is the means by which monetary transactions are effected." (Wikipedia, 2021).
+### redis
-Therefore, it is possible to imagine that new coins come into existence or cease to exist, it is also possible to imagine fictitious coins such as Dungeons & Dragons coins being used in these transactions, such as how much is a Gold Piece (Dungeons & Dragons) in Real or how much is the GTA$1 in Real.
+Banco de dados em memória (volátil) que dará melhor desempenho e velocidade em nosso response.
-Let's consider the PSN quote where GTA$1,250,000.00 cost R$83.50 we clearly have a relationship between the currencies, so it is possible to create a quote. (Playstation Store, 2021).
+### worker
-Ref:
-Wikipedia [Institutional Website]. Available at: . Accessed on: 28 April 2021.
-Playstation Store [Virtual Store]. Available at: . Accessed on: 28 April 2021.
+Serviço responsável por manter o db e o redis sempre atualizados com dados reais. O tempo de atualização default é de 5 minutos, porém esse parâmetro pode ser alterado atualizando o docker-compose.yml
-You can use any programming language for the challenge. Below is the list of languages that we here at Hurb have more affinity:
+### app
-- JavaScript (NodeJS)
-- Python
-- Go
-- Ruby
-- C++
-- PHP
+Nosso servidor backend escrito em NodeJS utilizando o framework [ExpressJS](https://expressjs.com/pt-br/).
-## Requirements
-- Fork this challenge and create your project (or workspace) using your version of that repository, as soon as you finish the challenge, submit a _pull request_.
- - If you have any reason not to submit a _pull request_, create a private repository on Github, do every challenge on the **main** branch and don't forget to fill in the `pull-request.txt` file. As soon as you finish your development, add the user `automator-hurb` to your repository as a contributor and make it available for at least 30 days. **Do not add the `automator-hurb` until development is complete.**
- - If you have any problem creating the private repository, at the end of the challenge fill in the file called `pull-request.txt`, compress the project folder - including the `.git` folder - and send it to us by email.
-- The code needs to run on macOS or Ubuntu (preferably as a Docker container)
-- To run your code, all you need to do is run the following commands:
- - git clone \$your-fork
- - cd \$your-fork
- - command to install dependencies
- - command to run the application
-- The API can be written with or without the help of _frameworks_
- - If you choose to use a _framework_ that results in _boilerplate code_, mark in the README which piece of code was written by you. The more code you make, the more content we will have to rate.
-- The API needs to support a volume of 1000 requests per second in a stress test.
-- The API needs to include real and current quotes through integration with public currency quote APIs
+## Executando o projeto
-## Evaluation criteria
+Para execução do projeto basta executarmos o comando:
+```shell
+docker compose up
+```
-- **Organization of code**: Separation of modules, view and model, back-end and front-end
-- **Clarity**: Does the README explain briefly what the problem is and how can I run the application?
-- **Assertiveness**: Is the application doing what is expected? If something is missing, does the README explain why?
-- **Code readability** (including comments)
-- **Security**: Are there any clear vulnerabilities?
-- **Test coverage** (We don't expect full coverage)
-- **History of commits** (structure and quality)
-- **UX**: Is the interface user-friendly and self-explanatory? Is the API intuitive?
-- **Technical choices**: Is the choice of libraries, database, architecture, etc. the best choice for the application?
+A ordem de execução será a seguinte:
+1. redis
+2. mysql
+3. worker
+4. app (com delay de 10 segundos, para que dê tempo do worker realizar as atualizações)
-## Doubts
+Para encerramento dos serviços:
+```shell
+docker compose down
+```
-Any questions you may have, check the [_issues_](https://github.com/HurbCom/challenge-bravo/issues) to see if someone hasn't already and if you can't find your answer, open one yourself. new issue!
+Se precisar alterar o fonte, envs, ou algum outro arquivo, para rebuildar os serviços, basta executar o comando abaixo:
+```shell
+docker compose up --build
+```
-Godspeed! ;)
+## API
-
-
-
+> [!IMPORTANT]
+> É necessário que os containers estejam em execução a partir deste momento
+
+O projeto estará executando na porta padrão 3000.
+
+O endereço padrão para testes:
+```
+http://localhost:3000/
+```
+
+### Documentação
+A documentação da API foi escrita utilizando a [OpenAPI 3.0 Specification](https://swagger.io/docs/specification/about/) e se encontra no endereço:
+```
+http://localhost:3000/api-docs/
+```
+ou
+
+[resume-doc.md](resume-doc.md)
+
+### Observações
+
+Para formatação monetária é utilizado o padrão brasileiro.
+
+Para moedas correntes foi utilzado o padrão de duas casas decimais, exemplo:
+
+```json
+{
+ "BRL": "R$ 1,00",
+ "USD": "US$ 0,20"
+}
+```
+
+para criptomoedas, utilizamos dez casas decimais:
+
+```json
+{
+ "BRL": "R$ 1,0000000000",
+ "BTC": "BTC 0,0000033427"
+}
+```
diff --git a/README.pt.md b/README.pt.md
deleted file mode 100644
index 0159db9f0..000000000
--- a/README.pt.md
+++ /dev/null
@@ -1,82 +0,0 @@
-#
Desafio Bravo
-
-[[English](README.md) | [Português](README.pt.md)]
-
-Construa uma API, que responda JSON, para conversão monetária. Ela deve ter uma moeda de lastro (USD) e fazer conversões entre diferentes moedas com **cotações de verdade e atuais**.
-
-A API precisa converter entre as seguintes moedas:
-
-- USD
-- BRL
-- EUR
-- BTC
-- ETH
-
-Outras moedas podem ser adicionadas conforme o uso.
-
-Ex: USD para BRL, USD para BTC, ETH para BRL, etc...
-
-A requisição deve receber como parâmetros: A moeda de origem, o valor a ser convertido e a moeda final.
-
-Ex: `?from=BTC&to=EUR&amount=123.45`
-
-Construa também um endpoint para adicionar e remover moedas suportadas pela API, usando os verbos HTTP.
-
-A API deve suportar conversão entre moedas fiduciárias, crypto e fictícias. Exemplo: BRL->HURB, HURB->ETH
-
-"Moeda é o meio pelo qual são efetuadas as transações monetárias." (Wikipedia, 2021).
-
-Sendo assim, é possível imaginar que novas moedas passem a existir ou deixem de existir, é possível também imaginar moedas fictícias como as de Dungeons & Dragons sendo utilizadas nestas transações, como por exemplo quanto vale uma Peça de Ouro (D&D) em Real ou quanto vale a GTA$ 1 em Real.
-
-Vamos considerar a cotação da PSN onde GTA$ 1.250.000,00 custam R$ 83,50 claramente temos uma relação entre as moedas, logo é possível criar uma cotação. (Playstation Store, 2021).
-
-Ref:
-Wikipedia [Site Institucional]. Disponível em: . Acesso em: 28 abril 2021.
-Playstation Store [Loja Virtual]. Disponível em: . Acesso em: 28 abril 2021.
-
-Você pode usar qualquer linguagem de programação para o desafio. Abaixo a lista de linguagens que nós aqui do Hurb temos mais afinidade:
-
-- JavaScript (NodeJS)
-- Python
-- Go
-- Ruby
-- C++
-- PHP
-
-## Requisitos
-
-- Forkar esse desafio e criar o seu projeto (ou workspace) usando a sua versão desse repositório, tão logo acabe o desafio, submeta um _pull request_.
- - Caso você tenha algum motivo para não submeter um _pull request_, crie um repositório privado no Github, faça todo desafio na branch **main** e não se esqueça de preencher o arquivo `pull-request.txt`. Tão logo termine seu desenvolvimento, adicione como colaborador o usuário `automator-hurb` no seu repositório e o deixe disponível por pelo menos 30 dias. **Não adicione o `automator-hurb` antes do término do desenvolvimento.**
- - Caso você tenha algum problema para criar o repositório privado, ao término do desafio preencha o arquivo chamado `pull-request.txt`, comprima a pasta do projeto - incluindo a pasta `.git` - e nos envie por email.
-- O código precisa rodar em macOS ou Ubuntu (preferencialmente como container Docker)
-- Para executar seu código, deve ser preciso apenas rodar os seguintes comandos:
- - git clone \$seu-fork
- - cd \$seu-fork
- - comando para instalar dependências
- - comando para executar a aplicação
-- A API pode ser escrita com ou sem a ajuda de _frameworks_
- - Se optar por usar um _framework_ que resulte em _boilerplate code_, assinale no README qual pedaço de código foi escrito por você. Quanto mais código feito por você, mais conteúdo teremos para avaliar.
-- A API precisa suportar um volume de 1000 requisições por segundo em um teste de estresse.
-- A API precisa contemplar cotações de verdade e atuais através de integração com APIs públicas de cotação de moedas
-
-## Critério de avaliação
-
-- **Organização do código**: Separação de módulos, view e model, back-end e front-end
-- **Clareza**: O README explica de forma resumida qual é o problema e como pode rodar a aplicação?
-- **Assertividade**: A aplicação está fazendo o que é esperado? Se tem algo faltando, o README explica o porquê?
-- **Legibilidade do código** (incluindo comentários)
-- **Segurança**: Existe alguma vulnerabilidade clara?
-- **Cobertura de testes** (Não esperamos cobertura completa)
-- **Histórico de commits** (estrutura e qualidade)
-- **UX**: A interface é de fácil uso e auto-explicativa? A API é intuitiva?
-- **Escolhas técnicas**: A escolha das bibliotecas, banco de dados, arquitetura, etc, é a melhor escolha para a aplicação?
-
-## Dúvidas
-
-Quaisquer dúvidas que você venha a ter, consulte as [_issues_](https://github.com/HurbCom/challenge-bravo/issues) para ver se alguém já não a fez e caso você não ache sua resposta, abra você mesmo uma nova issue!
-
-Boa sorte e boa viagem! ;)
-
-
-
-
diff --git a/ca.jpg b/ca.jpg
deleted file mode 100644
index 939944346..000000000
Binary files a/ca.jpg and /dev/null differ
diff --git a/controllers/currency_exchange.js b/controllers/currency_exchange.js
new file mode 100644
index 000000000..a06671761
--- /dev/null
+++ b/controllers/currency_exchange.js
@@ -0,0 +1,117 @@
+const Redis = require('../services/redis');
+const { format2float, formatCurrency, useCryptoFormat } = require('../utils/formatter');
+const { CurrencysModel, CurrencysRaw } = require('../models/currencys');
+
+const GetCurrency = async currency => {
+ try {
+ const result = await Redis.get(currency);
+ return result;
+ } catch (error) {
+ throw new Error(error);
+ }
+};
+
+const ConvertCurrency = async (from, to, amount) => {
+ try {
+ let calc = 0;
+ let crypto = from.crypto || to.crypto;
+ crypto = typeof crypto === 'boolean' ? crypto : crypto === 'true';
+
+ if (from.currency === to.currency) calc = amount;
+ else {
+ amount = format2float(amount, from.crypto);
+
+ const amountIdUSD = amount / from.ballast_usd;
+ calc = amountIdUSD * to.ballast_usd;
+ }
+
+ return {
+ from: formatCurrency(amount, from.currency, useCryptoFormat(amount, from.crypto)),
+ to: formatCurrency(calc, to.currency, useCryptoFormat(calc, to.crypto))
+ }
+ } catch (error) {
+ throw new Error(error);
+ }
+};
+
+const NewCurrency = async (currency, ballast_usd, crypto = false) => {
+ const transaction = await CurrencysRaw.transaction();
+ try {
+
+ let existCurrency = await CurrencysModel.findOne({ where: { currency: currency.toUpperCase() } });
+ if (existCurrency) {
+ return {
+ status: 409,
+ message: 'Currency already exists'
+ };
+ }
+
+ await CurrencysModel.create({
+ currency: currency.toUpperCase(),
+ ballast_usd: ballast_usd,
+ crypto,
+ imported: false,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }, { transaction });
+
+ await Redis.set(currency, JSON.stringify(
+ {
+ currency: currency.toUpperCase(),
+ ballast_usd,
+ crypto
+ }
+ ));
+
+ await transaction.commit();
+
+ return {
+ status: 201,
+ message: 'Currency created successfully'
+ };
+ } catch (error) {
+ await transaction.rollback();
+ throw new Error(error);
+ }
+};
+
+const DeleteCurrency = async currency => {
+ const transaction = await CurrencysRaw.transaction();
+ try {
+ const curr = await CurrencysModel.findOne({
+ where: {
+ currency: currency.toUpperCase()
+ }
+ });
+ if(!curr) {
+ return {
+ status: 404,
+ message: 'Currency not found'
+ };
+ }
+ if(curr.imported) {
+ return {
+ status: 403,
+ message: 'Not authorized delete default currencies'
+ };
+ }
+
+ await CurrencysModel.destroy({ where: { currency: currency.toUpperCase() }, transaction });
+ await Redis.set(currency, 0, 1);
+ await transaction.commit();
+ return {
+ status: 200,
+ message: 'Currency deleted successfully'
+ };
+ } catch (error) {
+ await transaction.rollback();
+ throw new Error(error);
+ }
+};
+
+module.exports = {
+ GetCurrency,
+ ConvertCurrency,
+ NewCurrency,
+ DeleteCurrency
+};
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..d55d4e126
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,80 @@
+version: '3.8'
+services:
+ redis:
+ container_name: redis
+ image: redis
+ command: redis-server --requirepass Redis2024!
+ ports:
+ - "6379:6379"
+ healthcheck:
+ test: ["CMD-SHELL", "redis-cli ping"]
+ interval: 10s
+ retries: 3
+ timeout: 5s
+
+ db:
+ image: mysql:8
+ container_name: mysql
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: rootpassword
+ MYSQL_DATABASE: currencydb
+ MYSQL_USER: user
+ MYSQL_PASSWORD: password
+ ports:
+ - "3306:3306"
+ volumes:
+ - ./mysql_data:/var/lib/mysql
+ - ./sql/currencydb.sql:/docker-entrypoint-initdb.d/currencydb.sql
+ healthcheck:
+ test: ["CMD-SHELL", "mysqladmin ping -h db -u user -ppassword"]
+ interval: 30s
+ timeout: 30s
+ retries: 10
+ start_interval: 30s
+
+ worker:
+ container_name: worker
+ build:
+ context: .
+ dockerfile: Dockerfile.worker
+ environment:
+ NODE_ENV: development
+ MYSQL_HOST: db
+ MYSQL_USER: user
+ MYSQL_PASSWORD: password
+ MYSQL_DATABASE: currencydb
+ REDIS_HOST_TLS: redis://redis
+ REDIS_PASSWORD_TLS: Redis2024!
+ AWAIT_DELAY_SECONDS: 300
+ command: node workers/currencys.js
+ depends_on:
+ db:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+
+ app:
+ container_name: backend
+ build:
+ context: .
+ dockerfile: Dockerfile.backend
+ ports:
+ - "3000:3000"
+ environment:
+ NODE_ENV: development
+ MYSQL_HOST: db
+ MYSQL_USER: user
+ MYSQL_PASSWORD: password
+ MYSQL_DATABASE: currencydb
+ REDIS_HOST_TLS: redis://redis
+ REDIS_PASSWORD_TLS: Redis2024!
+ PORT: 3000
+ command: bash -c "sleep 10; npm start"
+ depends_on:
+ db:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ worker:
+ condition: service_started
\ No newline at end of file
diff --git a/docs/Architecture.drawio b/docs/Architecture.drawio
new file mode 100644
index 000000000..b83a97490
--- /dev/null
+++ b/docs/Architecture.drawio
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/Architecture.png b/docs/Architecture.png
new file mode 100644
index 000000000..0ab3b3d5f
Binary files /dev/null and b/docs/Architecture.png differ
diff --git a/docs/Sequence-API.drawio b/docs/Sequence-API.drawio
new file mode 100644
index 000000000..fe7f0aa96
--- /dev/null
+++ b/docs/Sequence-API.drawio
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/Sequence-API.png b/docs/Sequence-API.png
new file mode 100644
index 000000000..1e51001c9
Binary files /dev/null and b/docs/Sequence-API.png differ
diff --git a/docs/Sequence-Worker.drawio b/docs/Sequence-Worker.drawio
new file mode 100644
index 000000000..43541924e
--- /dev/null
+++ b/docs/Sequence-Worker.drawio
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/Sequence-Worker.png b/docs/Sequence-Worker.png
new file mode 100644
index 000000000..42072380b
Binary files /dev/null and b/docs/Sequence-Worker.png differ
diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json
new file mode 100644
index 000000000..ec6702777
--- /dev/null
+++ b/docs/swagger/swagger.json
@@ -0,0 +1,339 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "Challenge-Bravo",
+ "description": "API for currency conversion.",
+ "contact": {
+ "name": "Leonardo Neves ",
+ "email": "neves.leo@outlook.com"
+ },
+ "version": "1.0.0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:3000/"
+ }
+ ],
+ "tags": [
+ {
+ "name": "/"
+ }
+ ],
+ "paths": {
+ "/": {
+ "get": {
+ "tags": [
+ "/"
+ ],
+ "summary": "Convert value between two currencies",
+ "description": "Convert value between two currencies",
+ "parameters": [
+ {
+ "name": "from",
+ "in": "query",
+ "description": "Currency origin",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "default": "BRL"
+ }
+ },
+ {
+ "name": "to",
+ "in": "query",
+ "description": "Currency destination",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "default": "USD"
+ }
+ },
+ {
+ "name": "amount",
+ "in": "query",
+ "description": "Amount to convert",
+ "required": true,
+ "schema": {
+ "type": "number",
+ "default": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Ok"
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ValidationResponseErrorArray"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Currency code not found"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Server internal error"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "/"
+ ],
+ "summary": "Add a new currency",
+ "description": "Add a new currency",
+ "requestBody": {
+ "description": "Create a new pet in the store",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Currency"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "description": "Created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Currency created successfully"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ValidationResponseErrorArray"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Conflict",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Currency already exists"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Server internal error"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "/"
+ ],
+ "summary": "Delete currency",
+ "description": "Delete currency",
+ "parameters": [
+ {
+ "name": "currency",
+ "in": "query",
+ "description": "Currency that needs to be deleted",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Ok",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Currency deleted successfully"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ValidationResponseErrorArray"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Not authorized delete default currencies"
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Currency code not found"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "default": "Server internal error"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Currency": {
+ "required": [
+ "currency",
+ "ballast_usd"
+ ],
+ "type": "object",
+ "properties": {
+ "currency": {
+ "type": "string",
+ "example": "GTA"
+ },
+ "ballast_usd": {
+ "type": "number",
+ "example": 0.000013544
+ },
+ "crypto": {
+ "type": "boolean",
+ "example": false
+ }
+ }
+ },
+ "ValidationResponseError": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "example": "field"
+ },
+ "msg": {
+ "type": "string",
+ "example": "Invalid value"
+ },
+ "path": {
+ "type": "string",
+ "example": "currency"
+ },
+ "location": {
+ "type": "string",
+ "example": "query"
+ }
+ }
+ },
+ "ValidationResponseErrorArray": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ValidationResponseError"
+ }
+ }
+ },
+ "requestBodies": {
+ "Currency": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Currency"
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/swagger/swagger.yml b/docs/swagger/swagger.yml
new file mode 100644
index 000000000..b6d8b51f5
--- /dev/null
+++ b/docs/swagger/swagger.yml
@@ -0,0 +1,212 @@
+openapi: 3.0.3
+info:
+ title: Challenge-Bravo
+ description: |-
+ API for currency conversion.
+ contact:
+ email: neves.leo@outlook.com
+ version: 1.0.0
+servers:
+ - url: http://localhost:3000/
+tags:
+ - name: /
+paths:
+ /:
+ get:
+ tags:
+ - /
+ summary: Convert value between two currencies
+ description: Convert value between two currencies
+ parameters:
+ - name: from
+ in: query
+ description: Currency origin
+ required: true
+ schema:
+ type: string
+ default: BRL
+ - name: to
+ in: query
+ description: Currency destination
+ required: true
+ schema:
+ type: string
+ default: USD
+ - name: amount
+ in: query
+ description: Amount to convert
+ required: true
+ schema:
+ type: number
+ default: 1
+ responses:
+ '200':
+ description: Ok
+ '400':
+ description: Bad Request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationResponseErrorArray'
+ '404':
+ description: Not found
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Currency code not found
+ '500':
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Server internal error
+ post:
+ tags:
+ - /
+ summary: Add a new currency
+ description: Add a new currency
+ requestBody:
+ description: Create a new pet in the store
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Currency'
+ required: true
+ responses:
+ '201':
+ description: Created
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Currency created successfully
+ '400':
+ description: Bad Request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationResponseErrorArray'
+ '409':
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Currency already exists
+ '500':
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Server internal error
+ delete:
+ tags:
+ - /
+ summary: Delete currency
+ description: Delete currency
+ parameters:
+ - name: currency
+ in: query
+ description: Currency that needs to be deleted
+ required: true
+ schema:
+ type: string
+ format: string
+ responses:
+ '200':
+ description: Ok
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Currency deleted successfully
+ '400':
+ description: Bad Request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ValidationResponseErrorArray'
+ '403':
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Not authorized delete default currencies
+ '404':
+ description: Not found
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Currency code not found
+ '500':
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ properties:
+ message:
+ type: string
+ default: Server internal error
+components:
+ schemas:
+ Currency:
+ required:
+ - currency
+ - ballast_usd
+ type: object
+ properties:
+ currency:
+ type: string
+ example: GTA
+ ballast_usd:
+ type: number
+ example: 0.000013544
+ crypto:
+ type: boolean
+ example: false
+ ValidationResponseError:
+ type: object
+ properties:
+ type:
+ type: string
+ example: field
+ msg:
+ type: string
+ example: Invalid value
+ path:
+ type: string
+ example: currency
+ location:
+ type: string
+ example: query
+ ValidationResponseErrorArray:
+ type: array
+ items:
+ $ref: '#/components/schemas/ValidationResponseError'
+ requestBodies:
+ Currency:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Currency'
\ No newline at end of file
diff --git a/models/connection.js b/models/connection.js
new file mode 100644
index 000000000..c6a4d4ba2
--- /dev/null
+++ b/models/connection.js
@@ -0,0 +1,13 @@
+const Sequelize = require('sequelize');
+
+module.exports.sequelize = new Sequelize(
+ process.env.MYSQL_DATABASE,
+ process.env.MYSQL_USER,
+ process.env.MYSQL_PASSWORD,
+ {
+ host: process.env.MYSQL_HOST,
+ dialect: 'mysql',
+ logging: false
+ }
+);
+
diff --git a/models/currencys.js b/models/currencys.js
new file mode 100644
index 000000000..a82712959
--- /dev/null
+++ b/models/currencys.js
@@ -0,0 +1,37 @@
+const Sequelize = require('sequelize');
+
+const { sequelize } = require('./connection');
+
+const CurrencysModel = sequelize.define('currencys', {
+ id: {
+ type: Sequelize.INTEGER,
+ autoIncrement: true,
+ allowNull: false,
+ primaryKey: true
+ },
+ currency: {
+ type: Sequelize.STRING(10),
+ allowNull: false
+ },
+ ballast_usd: {
+ type: Sequelize.STRING(45),
+ allowNull: false
+ },
+ crypto: {
+ type: Sequelize.BOOLEAN,
+ allowNull: false
+ },
+ imported: {
+ type: Sequelize.BOOLEAN,
+ allowNull: false
+ },
+ createdAt: { type: Sequelize.DATE },
+ updatedAt: { type: Sequelize.DATE }
+}, {
+ freezeTableName: true
+});
+
+module.exports = {
+ CurrencysModel,
+ CurrencysRaw: sequelize
+};
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..ee72dd5c2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "challenge-bravo",
+ "version": "1.0.0",
+ "description": "[[English](README.md) | [Portuguese](README.pt.md)]",
+ "main": "index.js",
+ "scripts": {
+ "test": "npm run test:unit && npm run test:integration",
+ "test:unit": "jest ./tests/unit/*.test.js",
+ "test:integration": "mocha tests/integration/*.test.js",
+ "start": "node server.js",
+ "debug": "nodemon server.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/nadoneves/challenge-bravo.git"
+ },
+ "keywords": [],
+ "author": "Leonardo Neves ",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/nadoneves/challenge-bravo/issues"
+ },
+ "homepage": "https://github.com/nadoneves/challenge-bravo#readme",
+ "dependencies": {
+ "axios": "^1.6.7",
+ "dotenv": "^16.4.5",
+ "express": "^4.18.2",
+ "express-rate-limit": "^7.1.5",
+ "express-validator": "^7.0.1",
+ "helmet": "^7.1.0",
+ "mysql2": "^3.9.1",
+ "redis": "^4.6.13",
+ "sequelize": "^6.37.1",
+ "swagger-ui-express": "^5.0.0"
+ },
+ "devDependencies": {
+ "chai": "^5.1.0",
+ "jest": "^29.7.0",
+ "mocha": "^10.3.0",
+ "supertest": "^6.3.4"
+ }
+}
diff --git a/pull-request.txt b/pull-request.txt
deleted file mode 100644
index 4eae37418..000000000
--- a/pull-request.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Your name: ___
-Your Github homepage: ___
-Original challenge URL: http://github.com/hurbcom/challenge-___
diff --git a/resume-doc.md b/resume-doc.md
new file mode 100644
index 000000000..eb856e92f
--- /dev/null
+++ b/resume-doc.md
@@ -0,0 +1,77 @@
+#### Documentação resumida
+
+> [!NOTE]
+> Para uma experiência mais completa acesse o endereço:
+> http://localhost:3000/api-docs/
+
+
+##### Solicitar uma conversão entre duas moedas
+
+```shell
+curl --request GET \
+ --url 'http://localhost:3000/?from=BRL&to=USD&amount=1'
+```
+Parâmetros:
+| Name | Located in | Description | Required | Schema |
+| ---- | ---------- | ----------- | -------- | ---- |
+| from | query | Moeda de origem | Yes | string |
+| to | query | Moeda de destino | Yes | string |
+| amount | query | Valor a ser convertido | Yes | number |
+
+Response:
+| Code | Description |
+| ---- | ----------- |
+| 200 | Ok |
+| 400 | Bad Request |
+| 404 | Not found |
+| 500 | Internal Server Error |
+
+##### Criar uma nova moeda
+
+```shell
+curl --request POST \
+ --url http://localhost:3000/ \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "currency": "GTA",
+ "ballast_usd": 0.000013544,
+ "crypto": false
+}'
+```
+Body:
+| Name | Located in | Description | Required | Schema |
+| ---- | ---------- | ----------- | -------- | ---- |
+| currency | body | Nome da moeda | Yes | string |
+| ballast_usd | body | Valor da moeds em Dólar Americano (USD) | Yes | number |
+| crypto | body | Se é uma criptomoeda | Yes | boolean |
+
+Response:
+| Code | Description |
+| ---- | ----------- |
+| 201 | Created |
+| 400 | Bad Request |
+| 409 | Conflict |
+| 500 | Internal Server Error |
+
+##### Remover uma moeda
+
+*Só poderão ser removidas as moedas criadas pelo usuário.
+**As moedas obtidas pelo [worker](#worker-1) não poderão ser removidas
+
+```shell
+curl --request DELETE \
+ --url 'http://localhost:3000/?currency=GTA'
+```
+Parâmetros:
+| Name | Located in | Description | Required | Schema |
+| ---- | ---------- | ----------- | -------- | ---- |
+| currency | query | Moeda a ser removida | Yes | string (string) |
+
+Response:
+| Code | Description |
+| ---- | ----------- |
+| 200 | Ok |
+| 400 | Bad Request |
+| 403 | Forbidden |
+| 404 | Not found |
+| 500 | Internal Server Error |
\ No newline at end of file
diff --git a/routes/main.js b/routes/main.js
new file mode 100644
index 000000000..70f374bdc
--- /dev/null
+++ b/routes/main.js
@@ -0,0 +1,106 @@
+const express = require('express');
+const router = express.Router();
+
+const { query, body, validationResult } = require('express-validator');
+
+const { Response } = require('../utils/response');
+const {
+ ConvertCurrency, GetCurrency, NewCurrency, DeleteCurrency
+} = require('../controllers/currency_exchange');
+
+router.get('/',
+ query('from').notEmpty().escape().isLength({ min: 1, max: 10 }).withMessage('Currency code \'from\' must be 1 characters long'),
+ query('to').notEmpty().escape().isLength({ min: 1, max: 10 }).withMessage('Currency code \'to\' must be 1 characters long'),
+ query('amount').notEmpty().escape().withMessage('Amount must be a number'),
+ async (req, res) => {
+ try {
+ const validateQuery = validationResult(req);
+ if (!validateQuery.isEmpty()) {
+ return Response(res, 400, validateQuery.array());
+ }
+
+ const amount = req.query.amount;
+
+ const fromPromise = GetCurrency(req.query.from.toUpperCase());
+ const toPromise = GetCurrency(req.query.to.toUpperCase());
+
+ let [from, to] = await Promise.all([fromPromise, toPromise]);
+
+ if(!from) return Response(res, 404, {message: `Currency code 'from' ${req.query.from} not found`});
+ if(!to) return Response(res, 404, {message: `Currency code 'to' ${req.query.to} not found`});
+
+ from = JSON.parse(from);
+ to = JSON.parse(to);
+
+ const converted = await ConvertCurrency(from, to, amount);
+
+ const result = {};
+ result[from.currency] = converted.from;
+ result[to.currency] = converted.to;
+
+ Response(res, 200, result);
+ } catch (error) {
+ console.error('routes/main.js ~ get ~ ERROR: ', error);
+ Response(res, 500, {message: "Internal server error"});
+ }
+ }
+);
+
+router.post('/',
+ body('currency').notEmpty().trim().escape().isLength({ min: 1, max: 10 }).withMessage('Currency code must be 3 characters long'),
+ body('ballast_usd').notEmpty().trim().escape().isLength({ min: 1 }).isFloat().withMessage('Ballast USD must be a number'),
+ body('crypto').optional().trim().escape().isLength({ min: 1 }).isBoolean().withMessage('Crypto must be a boolean'),
+ async (req, res) => {
+ try {
+ const validateBody = validationResult(req);
+ if (!validateBody.isEmpty()) {
+ return Response(res, 400, validateBody.array());
+ }
+
+ const currency = req.body.currency;
+ const ballast_usd = req.body.ballast_usd;
+ let crypto = req.body.crypto;
+ crypto = typeof crypto === 'undefined' ? false : crypto === 'true';
+
+ const result = await NewCurrency(
+ currency,
+ ballast_usd,
+ typeof crypto === 'undefined' ? false : crypto
+ );
+
+ Response(res, result.status, {message: result.message});
+ } catch (error) {
+ console.error('routes/main.js ~ post ~ ERROR: ', error);
+ Response(res, 500, {message: "Internal server error"});
+ }
+
+ }
+);
+
+router.delete('/',
+ query('currency').notEmpty().escape().isLength({ min: 1, max: 10 }).withMessage('Currency code \'from\' must be 1 characters long'),
+ async (req, res) => {
+ try {
+ const validateBody = validationResult(req);
+ if (!validateBody.isEmpty()) {
+ return Response(res, 400, validateBody.array());
+ }
+
+ const { currency } = req.query;
+
+ const existCurrency = await GetCurrency(currency.toUpperCase());
+
+ if(!existCurrency) return Response(res, 400, {message: `Currency code not found`});
+
+ const result = await DeleteCurrency(currency.toUpperCase());
+
+ Response(res, result.status, {message: result.message});
+ } catch (error) {
+ console.error('routes/main.js ~ delete ~ ERROR: ', error);
+ Response(res, 500, {message: "Internal server error"});
+ }
+
+ }
+);
+
+module.exports = router;
\ No newline at end of file
diff --git a/server.js b/server.js
new file mode 100644
index 000000000..94c8aaf8a
--- /dev/null
+++ b/server.js
@@ -0,0 +1,40 @@
+require('dotenv').config();
+
+const express = require('express');
+const helmet = require('helmet');
+const rateLimit = require('express-rate-limit');
+const swaggerUi = require('swagger-ui-express');
+const swaggerDocument = require('./docs/swagger/swagger.json');
+
+const app = express();
+const port = process.env.PORT || 3000;
+
+app.use(express.json());
+app.use(helmet());
+
+app.use((err, req, res, next) => {
+ if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
+ console.error(err);
+ return res.status(400).send({ message: err.message, body: err.body });
+ }
+ next();
+});
+
+const limiter = rateLimit({
+ windowMs: 1000,
+ limit: 10000,
+ standardHeaders: true,
+ message: 'You have exceeded your ~1000 requests per second limit.',
+});
+app.use(limiter);
+
+app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {explorer: true}));
+
+const mainRoutes = require('./routes/main');
+app.use('/', mainRoutes);
+
+app.listen(port, () => {
+ console.log(`API rodando em http://localhost:${port}`);
+});
+
+module.exports = app;
\ No newline at end of file
diff --git a/services/redis.js b/services/redis.js
new file mode 100755
index 000000000..ccf853979
--- /dev/null
+++ b/services/redis.js
@@ -0,0 +1,39 @@
+const redis = require("redis");
+
+module.exports = {
+ set: async (key, value, ttl = null) => {
+ try {
+ const redisClient = redis.createClient({
+ url: process.env.REDIS_HOST_TLS,
+ password: process.env.REDIS_PASSWORD_TLS
+ });
+
+ await redisClient.connect();
+
+ if(ttl)
+ await redisClient.set(key, value, { EX: ttl });
+ else
+ await redisClient.set(key, value);
+
+ await redisClient.disconnect();
+ } catch (error) {
+ throw new Error(error);
+ }
+ },
+ get: async (key) => {
+ try {
+ const redisClient = redis.createClient({
+ url: process.env.REDIS_HOST_TLS,
+ password: process.env.REDIS_PASSWORD_TLS
+ });
+
+ await redisClient.connect();
+
+ const msg = await redisClient.get(key);
+ await redisClient.disconnect();
+ return msg;
+ } catch (error) {
+ throw new Error(error);
+ }
+ }
+};
diff --git a/sql/currencydb.sql b/sql/currencydb.sql
new file mode 100644
index 000000000..74e8cde3f
--- /dev/null
+++ b/sql/currencydb.sql
@@ -0,0 +1,14 @@
+-- Create a new database
+CREATE DATABASE IF NOT EXISTS currencydb;
+USE currencydb;
+
+-- Create a table for storing users
+CREATE TABLE IF NOT EXISTS currencys (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ currency VARCHAR(10) NOT NULL,
+ ballast_usd VARCHAR(45) NOT NULL,
+ crypto BOOLEAN NOT NULL DEFAULT 0,
+ imported BOOLEAN NOT NULL DEFAULT 0,
+ createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file
diff --git a/tests/integration/integrations.test.js b/tests/integration/integrations.test.js
new file mode 100644
index 000000000..0e95ba433
--- /dev/null
+++ b/tests/integration/integrations.test.js
@@ -0,0 +1,69 @@
+'use strict';
+
+const request = require('supertest');
+const app = require('../../server'); // Assuming your Express app is defined in app.js
+
+
+
+describe('Integration tests', () => {
+ it('should return status code 404 (Not Found) when the route does not exist', async () => {
+ const { expect } = await import('chai');
+ const response = await request(app).get('/anything');
+ expect(response.status).to.equal(404);
+ });
+
+ it('should return status code 404 (Bad Request) on get exchange when the currency does not exist', async () => {
+ const { expect } = await import('chai');
+ const response = await request(app).get('/?from=INTTESTMON&to=USD&amount=1');
+ expect(response.status).to.equal(404);
+ expect(response.body.message).to.equal('Currency code \'from\' INTTESTMON not found');
+ });
+
+ it('should return status code 400 (Bad Request) on get exchange', async () => {
+ const { expect } = await import('chai');
+ const response = await request(app).get('/');
+ expect(response.status).to.equal(400);
+ });
+
+ it('should return status code 200 (OK) on get exchange', async () => {
+ const { expect } = await import('chai');
+ const response = await request(app).get('/?from=USD&to=USD&amount=1');
+ expect(response.status).to.equal(200);
+ response.body = JSON.stringify(response.body).replace(/\s/g, ' ');
+ expect(response.body).to.deep.equal('{"USD":"US$ 1,00"}');
+ });
+
+ it('should return status code 201 (Created) on create a new currency', async () => {
+ const { expect } = await import('chai');
+ const response = await request(app)
+ .post('/')
+ .set('Content-Type', 'application/json')
+ .send({currency: 'MONEYTEST', ballast_usd: 0.05, crypto: false});
+ expect(response.status).to.equal(201);
+ expect(response.body.message).to.deep.equal('Currency created successfully');
+ });
+
+ it('should return status code 409 (Conflict) on create a new currency', async () => {
+ const { expect } = await import('chai');
+ const response = await request(app)
+ .post('/')
+ .set('Content-Type', 'application/json')
+ .send({currency: 'MONEYTEST', ballast_usd: 0.05, crypto: false});
+ expect(response.status).to.equal(409);
+ expect(response.body.message).to.deep.equal('Currency already exists');
+ });
+
+ it('should return status code 200 (OK) on remove a currency added manually', async () => {
+ const { expect } = await import('chai');
+ const response = await request(app).delete('/?currency=MONEYTEST');
+ expect(response.status).to.equal(200);
+ expect(response.body.message).to.deep.equal('Currency deleted successfully');
+ });
+
+ it('should return status code 403 (Forbidden) on remove a imported currency', async () => {
+ const { expect } = await import('chai');
+ const response = await request(app).delete('/?currency=USD');
+ expect(response.status).to.equal(403);
+ expect(response.body.message).to.deep.equal('Not authorized delete default currencies');
+ });
+});
\ No newline at end of file
diff --git a/tests/stress_test.yml b/tests/stress_test.yml
new file mode 100644
index 000000000..aae4205da
--- /dev/null
+++ b/tests/stress_test.yml
@@ -0,0 +1,19 @@
+config:
+ target: 'http://192.168.1.2:3000'
+ phases:
+ # - duration: 10
+ # arrivalRate: 10
+ # name: Warm up
+ - duration: 20
+ arrivalRate: 100
+ maxVusers: 1000
+ name: Ramp up load
+
+scenarios:
+ - flow:
+ - loop:
+ - get:
+ url: '/?from=USD&to=BRL&amount=1'
+ headers:
+ User-Agent: 'Artillery'
+ count: 10
\ No newline at end of file
diff --git a/tests/unit/convert_currency.test.js b/tests/unit/convert_currency.test.js
new file mode 100644
index 000000000..1a71e6a5d
--- /dev/null
+++ b/tests/unit/convert_currency.test.js
@@ -0,0 +1,55 @@
+const { ConvertCurrency } = require('../../controllers/currency_exchange');
+
+test('BRL: It should show the original monetary value of 100,00', async () => {
+ const result = await ConvertCurrency(
+ {"currency":"BRL","ballast_usd":4.93119,"crypto":false},
+ {"currency":"USD","ballast_usd":1,"crypto":false},
+ 100
+ );
+ expect(result.from.split(/\s/)[1]).toBe('100,00');
+});
+
+test('BRL to USD: It should show the destination monetary value of 20,28', async () => {
+ const result = await ConvertCurrency(
+ {"currency":"BRL","ballast_usd":4.93119,"crypto":false},
+ {"currency":"USD","ballast_usd":1,"crypto":false},
+ 100
+ );
+ expect(result.to.split(/\s/)[1]).toBe('20,28');
+});
+
+test('GTA: It should show the original monetary value of 1.250.00,00', async () => {
+ const result = await ConvertCurrency(
+ {"currency":"GTA","ballast_usd":73833.43178,"crypto":false},
+ {"currency":"USD","ballast_usd":1,"crypto":false},
+ 1250000
+ );
+ expect(result.from.split(/\s/)[1]).toBe('1.250.000,00');
+});
+
+test('GTA to USD: It should show the original monetary value of 16,93', async () => {
+ const result = await ConvertCurrency(
+ {"currency":"GTA","ballast_usd":73833.43178,"crypto":false},
+ {"currency":"USD","ballast_usd":1,"crypto":false},
+ 1250000
+ );
+ expect(result.to.split(/\s/)[1]).toBe('16,93');
+});
+
+test('GTA: It should show the original monetary value of 1.250.00,00', async () => {
+ const result = await ConvertCurrency(
+ {"currency":"GTA","ballast_usd":73833.43178,"crypto":false},
+ {"currency":"BRL","ballast_usd":4.93119,"crypto":false},
+ 1250000
+ );
+ expect(result.from.split(/\s/)[1]).toBe('1.250.000,00');
+});
+
+test('GTA to BRL: It should show the original monetary value of 83,49', async () => {
+ const result = await ConvertCurrency(
+ {"currency":"GTA","ballast_usd":73833.43178,"crypto":false},
+ {"currency":"BRL","ballast_usd":4.93119,"crypto":false},
+ 1250000
+ );
+ expect(result.to.split(/\s/)[1]).toBe('83,49');
+});
\ No newline at end of file
diff --git a/utils/formatter.js b/utils/formatter.js
new file mode 100644
index 000000000..7e733d404
--- /dev/null
+++ b/utils/formatter.js
@@ -0,0 +1,44 @@
+
+const format2float = (amount, crypto = false) => {
+ amount = amount.toString();
+ const commaIndex = amount.indexOf(',');
+ const dotIndex = amount.indexOf('.');
+
+ if(commaIndex < dotIndex) amount = amount.replace(/\,/g, '');
+ else amount = amount.replace(/\./g, '').replace(',', '.');
+
+ if(!crypto) amount = parseFloat(parseFloat(amount).toFixed(2));
+ else amount = parseFloat(amount);
+
+ return amount;
+};
+
+const formatCurrency = (amount, currency = 'USD', crypto = false) => {
+ amount = format2float(amount, crypto);
+
+ let minimumFractionDigits = 2;
+ if(crypto) minimumFractionDigits = 10;
+
+ try {
+ return amount.toLocaleString('pt-BR', { style: 'currency', currency: currency, minimumFractionDigits });
+ } catch (error) {
+ return "$ " + amount;
+ }
+};
+
+const useCryptoFormat = (value, crypto) => {
+ if(!value.toString().includes('.')) return false;
+ else {
+ const dotSepareted = value.toString().split('.');
+ if (dotSepareted[0] !== '0') return false;
+
+ if(!crypto) return false;
+ return dotSepareted[1].length > 2;
+ }
+};
+
+module.exports = {
+ format2float,
+ formatCurrency,
+ useCryptoFormat
+};
diff --git a/utils/response.js b/utils/response.js
new file mode 100644
index 000000000..2ead7475b
--- /dev/null
+++ b/utils/response.js
@@ -0,0 +1,7 @@
+exports.Response = (res, statusCode, data = null, cors = true) => {
+ if(cors)
+ res.set('Access-Control-Allow-Origin', '*');
+
+ if(!data) res.status(statusCode).send();
+ else res.status(statusCode).send(data);
+};
\ No newline at end of file
diff --git a/workers/currencys.js b/workers/currencys.js
new file mode 100644
index 000000000..737702ce1
--- /dev/null
+++ b/workers/currencys.js
@@ -0,0 +1,137 @@
+require('dotenv').config();
+
+const axios = require('axios');
+const redis = require("redis");
+const { CurrencysModel, CurrencysRaw } = require('../models/currencys');
+
+const AWAIT_DELAY_SECONDS = process.env.AWAIT_DELAY_SECONDS || 300;
+
+const sleep = (ms) => {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+};
+
+const getBallastDefault = async () => {
+ try {
+ const { data } = await axios.get(`https://api.coingate.com/v2/rates`);
+ const {BTC, ETH, USD} = data.merchant;
+
+ return {
+ BTC: 1 / BTC.USD,
+ ETH: 1 / ETH.USD,
+ USD: 1,
+ BRL: USD.BRL * 1,
+ EUR: USD.EUR * 1
+ }
+ } catch (error) {
+ throw new Error(error);
+ }
+}
+
+const setCurrencyRedis = async (key, value) => {
+ try {
+ const redisClient = redis.createClient({
+ url: process.env.REDIS_HOST_TLS,
+ password: process.env.REDIS_PASSWORD_TLS
+ });
+
+ await redisClient.connect();
+
+ await redisClient.set(key, value);
+
+ await redisClient.disconnect();
+ } catch (error) {
+ throw new Error(error);
+ }
+};
+
+const selectedCurrencys = [
+ { currency:'USD', crypto: false },
+ { currency:'BRL', crypto: false },
+ { currency:'EUR', crypto: false },
+ { currency:'BTC', crypto: true },
+ { currency:'ETH', crypto: true }
+];
+
+const handler = async () => {
+ while (true) {
+
+ const transaction = await CurrencysRaw.transaction();
+
+ try {
+ console.log('>> GET CURRENCYS ON PUBLIC API AND POPULATE DATABASE/REDIS');
+
+ const ballastDefault = await getBallastDefault();
+ const currencys = Object.keys(ballastDefault);
+ const countDataInDB = await CurrencysModel.findAll();
+ if(countDataInDB.length === 0) {
+ console.log('>>> POPULATE DATABASE');
+ for (let i = 0; i < currencys.length; i++) {
+ const currency = currencys[i];
+
+ const defaultCurrency = selectedCurrencys.find(f => f.currency == currency.toUpperCase());
+ if(!defaultCurrency) continue;
+
+ await CurrencysModel.create({
+ currency: defaultCurrency.currency,
+ ballast_usd: ballastDefault[currency],
+ crypto: defaultCurrency.crypto,
+ imported: true,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }, { transaction });
+ }
+ console.log('>>> POPULATE DATABASE COMPLETED');
+ } else {
+ console.log('>>> UPDATE');
+ for (let i = 0; i < currencys.length; i++) {
+ const currency = currencys[i];
+
+ const defaultCurrency = selectedCurrencys.find(f => f.currency == currency.toUpperCase());
+ if(!defaultCurrency) continue;
+
+ await CurrencysModel.update(
+ {
+ ballast_usd: ballastDefault[currency],
+ crypto: defaultCurrency.crypto,
+ imported: true,
+ updatedAt: new Date()
+ },
+ {
+ where: { currency: currency.toUpperCase() },
+ transaction
+ }
+ );
+ }
+ console.log('>>> UPDATE COMPLETED');
+ }
+
+ await transaction.commit();
+
+ console.log('>>> UPDATE REDIS DATABASE');
+ const currencysInDB = await CurrencysModel.findAll();
+ for (let i = 0; i < currencysInDB.length; i++) {
+ const currency = currencysInDB[i];
+ await setCurrencyRedis(currency.currency,
+ JSON.stringify(
+ {
+ currency: currency.currency,
+ ballast_usd: parseFloat(currency.ballast_usd),
+ crypto: currency.crypto
+ }
+ )
+ );
+ }
+ console.log('>>> UPDATE REDIS DATABASE COMPLETED');
+ } catch (error) {
+ console.error('>> ERRO AO CONSULTAR DADOS', error);
+ await transaction.rollback();
+ }
+
+ console.log('>> AWAITING ' + AWAIT_DELAY_SECONDS + ' SECONDS TO NEXT RUN');
+ await sleep(AWAIT_DELAY_SECONDS * 1000);
+ }
+};
+
+handler();
\ No newline at end of file