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 @@ # Hurb 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**. +

+ Architecture +

-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. +

+ [UML] Sequence - Worker +

-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. +

+ [UML] Sequence - Worker +

-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 -

- Challange accepted -

+> [!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 @@ -# Hurb 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! ;) - -

- Challange accepted -

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