Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Marcelo Senna - Relização do Teste Técnico #16

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/

resultMockup.json
dataMockup.csv
superBigMockup.csv
34 changes: 34 additions & 0 deletions Challenge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Teste Prático para Desenvolvedor Full Stack Kronoos

Você foi designado para desenvolver uma aplicação que deve lidar com grandes volumes de dados. Você deve rodar as seguintes validações e tratativas para cada um dos dados do arquivo e mostrar um retorno ao concluir a rotina. A aplicação será responsável por fornecer uma massa de dados considerável (cerca de 30GB) e deve ser capaz de lidar com dados fornecidos.

*Observação Importante:*
1. Pedimos extremo comprometimento com o teste, e utilizamos IA para validar se os testes foram gerados por alguma IA (ChatGPT, LhamaGPT, Bard, Jasper, entre outras). Sua dedicação será crucial para uma avaliação justa.
2. Pedimos que não utilize bibliotecas para efetuar a validação do CPF ou CNPJ. Queremos que você desenvolva o seu próprio algoritmo de validação para que possamos entender qual sua dinâmica de raciocínio.
3. Pedimos que clonem o repo ou façam um fork para o github pessoal e nos sinalizem quando finalizarem, pois não será possível abrir PR neste repositório do teste.

## Manipulação de Dados de CSV e Conversão para Array

- Os dados são fornecidos em formato CSV.
- Utilizaremos a biblioteca fs (File System) para ler o arquivo CSV e a biblioteca csv-parser para processar os dados e convertê-los em um array de objetos JavaScript.

## Conversão de Dados para Moeda Real Brasileira

- Valores monetários, como vlTotal, vlPresta, vlMora, etc., precisam ser formatados como moeda brasileira (BRL).
- Utilizaremos a biblioteca intl do JavaScript para formatar os valores numéricos como moeda BRL, incluindo o símbolo de real (R$), separador de milhar e precisão de duas casas decimais.

## Validação de CPF ou CNPJ

- Implementaremos uma função para validar o campo nrCpfCnpj e verificar se ele é um CPF ou CNPJ válido, seguindo as regras de validação apropriadas para cada formato.
- Parte de todos os CPF e CNPJ sao invalidos, usamos um script para gerar dados fictícios.

## Validação de Valor Total e Prestações

- Dividiremos o valor de `vlTotal` pela quantidade de prestações (`qtPrestacoes`).
- Verificaremos se o resultado dessa divisão é igual ao valor de `vlPresta` para cada prestação, garantindo que os cálculos estejam corretos e consistentes.

---

A conclusão bem-sucedida deste teste será avaliada com base na implementação eficiente de conceitos como tratamento de dados em larga escala, comunicação assíncrona, gerenciamento de estado, manipulação de CSV, escolha adequada de tecnologias e boas práticas de desenvolvimento.

Boa sorte!
125 changes: 104 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,117 @@
# Teste Prático para Desenvolvedor Full Stack Kronoos
## Como rodar a aplicação.

Você foi designado para desenvolver uma aplicação que deve lidar com grandes volumes de dados. Você deve rodar as seguintes validações e tratativas para cada um dos dados do arquivo e mostrar um retorno ao concluir a rotina. A aplicação será responsável por fornecer uma massa de dados considerável (cerca de 30GB) e deve ser capaz de lidar com dados fornecidos.
Clone o repositório da aplicação.

*Observação Importante:*
1. Pedimos extremo comprometimento com o teste, e utilizamos IA para validar se os testes foram gerados por alguma IA (ChatGPT, LhamaGPT, Bard, Jasper, entre outras). Sua dedicação será crucial para uma avaliação justa.
2. Pedimos que não utilize bibliotecas para efetuar a validação do CPF ou CNPJ. Queremos que você desenvolva o seu próprio algoritmo de validação para que possamos entender qual sua dinâmica de raciocínio.
3. Pedimos que clonem o repo ou façam um fork para o github pessoal e nos sinalizem quando finalizarem, pois não será possível abrir PR neste repositório do teste.
`git clone https://github.com/Heinrick-Senna/teste-vaga-fullstack`

## Manipulação de Dados de CSV e Conversão para Array
Entre na pasta clonada

- Os dados são fornecidos em formato CSV.
- Utilizaremos a biblioteca fs (File System) para ler o arquivo CSV e a biblioteca csv-parser para processar os dados e convertê-los em um array de objetos JavaScript.
`cd teste-vaga-fullstack`

## Conversão de Dados para Moeda Real Brasileira
Instale as dependências do projeto

- Valores monetários, como vlTotal, vlPresta, vlMora, etc., precisam ser formatados como moeda brasileira (BRL).
- Utilizaremos a biblioteca intl do JavaScript para formatar os valores numéricos como moeda BRL, incluindo o símbolo de real (R$), separador de milhar e precisão de duas casas decimais.
`npm i` || `yarn install`

## Validação de CPF ou CNPJ
Instale o próprio projeto como uma dependência global

- Implementaremos uma função para validar o campo nrCpfCnpj e verificar se ele é um CPF ou CNPJ válido, seguindo as regras de validação apropriadas para cada formato.
- Parte de todos os CPF e CNPJ sao invalidos, usamos um script para gerar dados fictícios.
`npm i -g .` || `yarn global add .`

## Validação de Valor Total e Prestações
<hr>

- Dividiremos o valor de `vlTotal` pela quantidade de prestações (`qtPrestacoes`).
- Verificaremos se o resultado dessa divisão é igual ao valor de `vlPresta` para cada prestação, garantindo que os cálculos estejam corretos e consistentes.
Depois de feita a instalação do pacote globalmente você poderá usar o comando csvApp para interagir com a CLI e realizar as ações.

---
<img src="https://github.com/Heinrick-Senna/teste-vaga-fullstack/blob/main/images/CLI%20Example.png" />

A conclusão bem-sucedida deste teste será avaliada com base na implementação eficiente de conceitos como tratamento de dados em larga escala, comunicação assíncrona, gerenciamento de estado, manipulação de CSV, escolha adequada de tecnologias e boas práticas de desenvolvimento.
<hr>

Boa sorte!
## Exemplos de uso

`csvApp count -f .\data.csv` Contar a quantidade de linhas do arquivo data.csv

`csvApp generateMockup -f .\data.csv -o .\resultMockup.csv -n 1000000` Gerar um arquivo de exemplo com 1 milhão de linhas. Ele usará o header e a primeira linha do arquivo de origem.

`csvApp -f .\data.csv -o .\result.json` Por fim o comando default da CLI realiza uma verificação linha a linha de todo o arquivo data.csv e irá gerar como resultado um arquivo result.json que foi tratado conforme solicitado.<br/><b>Vale mencionar que ele também gera um objeto localizado ao final do json que mostra a quantidade de parcelas válidas e a quantidade de documentos válidos.</b>

Ex de um objeto inserido dentro de result.json
```
[
{
"nrInst": "533",
"nrAgencia": "32",
"cdClient": "56133",
"nmClient": "CLIENTE 1",
"nrCpfCnpj": "41854274761",
"nrContrato": "733067",
"dtContrato": "20221227",
"qtPrestacoes": "5",
"vlTotal": "R$ 83.720,19",
"cdProduto": "777",
"dsProduto": "CDC PESSOA JURIDICA",
"cdCarteira": "17",
"dsCarteira": "CRÉDITO DIRETO AO CONSUMIDOR",
"nrProposta": "798586",
"nrPresta": "2",
"tpPresta": "Original",
"nrSeqPre": "0",
"dtVctPre": "20220406",
"vlPresta": "R$ 17.524,03",
"vlMora": "R$ 29.196,96",
"vlMulta": "R$ 536,40",
"vlOutAcr": "R$ 0,00",
"vlIof": "R$ 0,00",
"vlDescon": "R$ 0,00",
"vlAtual": "R$ 47.257,39",
"idSituac": "Aberta",
"idSitVen": "Vencida",
"Parcela é Valida": false,
"CPF/CNPJ é Válido": false
},
{
"validDocuments": "13",
"validInstallments": "0"
}
]
```

## Sobre a capacidade da aplicação

### Foram realizados testes de criação e leitura de arquivos, sendo antigido as seguintes métricas.
- Tamanho máximo de arquivo Mockup.csv gravado: <b> 188GB (1 Bilhão de linhas) </b>

- Tamanho máximo de arquivo Mockup.csv lido: <b> 1.32GB (7 Milhões de linhas) </b>

- Tamanho máximo de arquivo Result.json processado: <b> 4.32GB </b>

<img src="https://github.com/Heinrick-Senna/teste-vaga-fullstack/blob/main/images/File%20Sizes.png" />

## Considerações Finais

### Referências usadas no projeto
- https://www.youtube.com/watch?v=O37n35XUxj0

- https://github.com/albertosouza/mini-cursos/blob/master/nodejs/lendo-arquivos-grandes.md

- https://stateful.com/blog/process-large-files-nodejs-streams

- https://stackoverflow.com/questions/44279211/how-to-read-big-files-in-nodejs

- https://www.youtube.com/watch?v=wmLLw3SnPfc

- https://www.youtube.com/watch?v=1a2ADqte1jY&t=132s

### Bibliotecas usadas no projeto
- [cli-progress](https://www.npmjs.com/package/cli-progress) -> Para construir e gerenciar a barra de progresso da aplicação.

- [csv-parsers](https://www.npmjs.com/package/csv-parser) -> Para processar e ler os arquivos .csv.

- [yargs](https://www.npmjs.com/package/yargs) -> Para facilitar a criação da CLI e leitura de seus respectivos comandos e argumentos.

### Perguntas gerais

- <b>Horas de dedicação</b><br/>8 Horas

- <b>Por que não usar Typescript?</b><br/>Complexidade desnecessária para um MVP de um projeto pequeno.

- <b>Oque a aplicação é capaz de fazer?</b><br/>Como foi mencionado ela é capaz de gerar CSVs de até 1 Bilhão de linhas e ler CSVs de até 7 Milhões de linhas, porém estes não são os limites da aplicação, na verdade esse é o máximo que eu cheguei à testar. Considerando também o real uso dessa aplicação, é possível a alteração da regra de negócio atual. A aplicação tem potêncial de expansão e flexibilidade para outras tratativas de dados e vínculo com bancos de dados por exemplo.

- <b>Essa aplicação pode ser usada em um ambiente real?</b><br />Creio que sim, até onde eu testei a aplicação tem consistência nos resultados apresentados e teria sim uma possibilidade de integração num ambiente de trabalho real.
39 changes: 39 additions & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env node
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import { mainFunctions } from './mainApp.js';

// Setando as configurações da CLI.
const argv = yargs(hideBin(process.argv))
.usage('Uso: csvApp <command> [options]')
.command('$0', 'Gera um novo arquivo com a validação dos dados.')
.command('generateMockup', 'Gerar um arquivo grande para teste.')
.command('count', 'Contar quantidade de linhas do arquivo.')
.nargs('f', 1)
.alias('f', 'file')
.describe('f', 'Arquivo a ser carregado')
.nargs('o', 1)
.alias('o', 'output')
.describe('o', 'Nome do arquivo a ser criado')
.nargs('n', 1)
.alias('n', 'number')
.describe('n', 'Número de linhas que serão geradas no arquivo de teste')
.demandOption(['f'])
.help('h')
.alias('h', 'help').argv


// Iniciando os comandos da aplicação
const handleCommand = async (command) => {
if (command != 'count' && !argv.output) return console.error('É necessário passar um caminho de destino com a flag -o')

const newApp = new mainFunctions(argv.file, argv.output);
await newApp.count();

if (command == 'count') return console.log(newApp.numLines);
if (command == 'generateMockup') return newApp.generateMockup(argv.n || 1000000);

newApp.main();
}

handleCommand(argv['_'][0])
130 changes: 130 additions & 0 deletions bin/mainApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import fs, { createWriteStream } from 'fs'
import path from 'path'
import cliProgress from 'cli-progress';
import csvParser from 'csv-parser'
import { createInterface } from 'readline';
import { formatCashValue, validateDocument } from './utils.js'
import { nextTick } from 'process';

// Create My Main functions;
export class mainFunctions {
constructor(filePath, outputPath) {
this.filePath = path.resolve(process.cwd(), filePath);
this.outputPath = outputPath ? path.resolve(process.cwd(), outputPath) : null;
this.numLines = 1;
this.progressBar = new cliProgress.SingleBar({ }, cliProgress.Presets.shades_classic);
}

async count () {
const readStream = fs.createReadStream(this.filePath);
return await new Promise(resolve => {
readStream.on('data', buf => this.numLines += buf.toString().match(/\n/g).length );
readStream.on('end', resolve );
})
}

// @n é o número de linhas que vamos gerar com a nossa aplicação...
generateMockup (n) {
this.progressBar.start(n, 1);

const readStream = fs.createReadStream(this.filePath);
const writeStream = fs.createWriteStream(this.outputPath);

let headers = '', dataToWrite = '';
readStream.on('data', (chunk) => {
let lines = chunk.toString().split('\n');
headers = lines[0];
dataToWrite = lines[1];
readStream.pause();
});

readStream.on('pause', async () => {
writeStream.write(`${headers}\n`);
for (let i = 1; i < n; i++) {
const overWatermark = writeStream.write(`${dataToWrite}\n`);

if (!overWatermark) {
await new Promise(resolve => {
writeStream.once('drain', resolve)
})
}

this.progressBar.increment()
}

writeStream.end();
});

writeStream.on('finish', () => {
nextTick(() => {
writeStream.close();
readStream.close();
this.progressBar.stop();
})
});
}

async main () {
this.progressBar.start(this.numLines, 1);

const readStream = fs.createReadStream(this.filePath);
const parsedStream = readStream.pipe(csvParser());
const writeStream = fs.createWriteStream(this.outputPath);

let validDocuments = 0, validInstallments = 0;
writeStream.write('[');
parsedStream.on('data', row => {
const installmentValidation = (parseFloat(row['vlTotal']) / parseInt(row['qtPrestacoes'])) == parseFloat(row['vlPresta']);
const documentValidation = validateDocument(row['nrCpfCnpj']);
this.progressBar.increment();

Object.keys(row).map(key => key.indexOf('vl') == 0 ? row[key] = formatCashValue(row[key]) : null );


const jsonToWrite = {
...row,
"Parcela é Valida": installmentValidation,
"CPF/CNPJ é Válido": documentValidation
}

if (installmentValidation) validInstallments++;
if (documentValidation) validDocuments++;

const overWatermark = writeStream.write(JSON.stringify(jsonToWrite)+',');
if (!overWatermark) {
parsedStream.pause();
writeStream.once('drain', () => parsedStream.resume());
}
});

writeStream.on('finish', () => {
nextTick(() => {
writeStream.close();
readStream.close();
this.progressBar.stop();
})
});

// Infelizmente a biblioteca csv-parser não detecta o real fim
// do parsing de todos os chunks ao emitir o Event 'end' assim como é previsto na documentação deles.
parsedStream.on('end', () => {
setTimeout(() => {
writeStream.write(`{"validDocuments":"${validDocuments}","validInstallments":"${validInstallments}"}]`);
writeStream.end();
this.progressBar.stop();
process.exit(0);
}, 1000)
});

// Não consegui implementar uma solução que le-se arquivos com mais de 1GB usando o csv-parser
// No entanto segue uma solução onde cada linha é lida, válidada e escrita individualmente, usando o createInterface
// da biblioteca readline;
// const rl = createInterface({
// input: readStream,
// })
// rl.on('line', (line) => {
// writeStream.write(line + '\n');
// this.progressBar.increment();
// })
}
}
Loading