@@ -0,0 +1,9 @@ | |||||
root = true | |||||
[*] | |||||
indent_style = space | |||||
indent_size = 2 | |||||
charset = utf-8 | |||||
trim_trailing_whitespace = true | |||||
insert_final_newline = true | |||||
end_of_line = lf |
@@ -0,0 +1,4 @@ | |||||
/*.js | |||||
node_modules | |||||
dist | |||||
@types |
@@ -0,0 +1,67 @@ | |||||
{ | |||||
"env": { | |||||
"es2020": true, | |||||
"node": true, | |||||
"jest": true | |||||
}, | |||||
"extends": [ | |||||
"airbnb-base", | |||||
"plugin:@typescript-eslint/recommended", | |||||
"prettier/@typescript-eslint", | |||||
"plugin:prettier/recommended" | |||||
], | |||||
"parser": "@typescript-eslint/parser", | |||||
"parserOptions": { | |||||
"ecmaVersion": 2020, | |||||
"sourceType": "module" | |||||
}, | |||||
"plugins": [ | |||||
"@typescript-eslint", | |||||
"prettier" | |||||
], | |||||
"rules": { | |||||
"prettier/prettier": "error", | |||||
"no-param-reassign": "off", | |||||
"no-plusplus": "off", | |||||
"no-await-in-loop": "off", | |||||
"no-useless-constructor": "off", | |||||
"no-underscore-dangle": "off", | |||||
"camelcase": "off", | |||||
"no-console": "off", | |||||
"class-methods-use-this": "off", | |||||
"@typescript-eslint/no-unused-vars": [ | |||||
"error", | |||||
{ | |||||
"argsIgnorePattern": "^_" | |||||
} | |||||
], | |||||
"@typescript-eslint/naming-convention": [ | |||||
"error", | |||||
{ | |||||
"selector": "interface", | |||||
"format": [ | |||||
"PascalCase" | |||||
], | |||||
"custom": { | |||||
"regex": "^I[A-Z]", | |||||
"match": true | |||||
} | |||||
} | |||||
], | |||||
"import/extensions": [ | |||||
"error", | |||||
"ignorePackages", | |||||
{ | |||||
"ts": "never" | |||||
} | |||||
], | |||||
"no-shadow": "off", | |||||
"@typescript-eslint/no-shadow": ["error"] | |||||
}, | |||||
"settings": { | |||||
"import/resolver": { | |||||
"typescript": {} | |||||
} | |||||
} | |||||
} |
@@ -1,109 +1,5 @@ | |||||
# ---> Node | |||||
# Logs | |||||
logs | |||||
node_modules | |||||
dist | |||||
*.log | *.log | ||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
lerna-debug.log* | |||||
# Diagnostic reports (https://nodejs.org/api/report.html) | |||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
*.logs | |||||
coverage | coverage | ||||
*.lcov | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# TypeScript v1 declaration files | |||||
typings/ | |||||
# TypeScript cache | |||||
*.tsbuildinfo | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Microbundle cache | |||||
.rpt2_cache/ | |||||
.rts2_cache_cjs/ | |||||
.rts2_cache_es/ | |||||
.rts2_cache_umd/ | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# Yarn Integrity file | |||||
.yarn-integrity | |||||
# dotenv environment variables file | |||||
.env | |||||
.env.test | |||||
# parcel-bundler cache (https://parceljs.org/) | |||||
.cache | |||||
# Next.js build output | |||||
.next | |||||
# Nuxt.js build / generate output | |||||
.nuxt | |||||
dist | |||||
# Gatsby files | |||||
.cache/ | |||||
# Comment in the public line in if your project uses Gatsby and not Next.js | |||||
# https://nextjs.org/blog/next-9-1#public-directory-support | |||||
# public | |||||
# vuepress build output | |||||
.vuepress/dist | |||||
# Serverless directories | |||||
.serverless/ | |||||
# FuseBox cache | |||||
.fusebox/ | |||||
# DynamoDB Local files | |||||
.dynamodb/ | |||||
# TernJS port file | |||||
.tern-port | |||||
# Stores VSCode versions used for testing VSCode extensions | |||||
.vscode-test | |||||
@@ -0,0 +1,11 @@ | |||||
FROM node:14.15.1 | |||||
WORKDIR /app | |||||
RUN apt-get update | |||||
RUN apt-get install build-essential libpoppler-cpp-dev pkg-config python-dev -y | |||||
RUN apt install poppler-utils -y | |||||
EXPOSE 3333 | |||||
CMD yarn && yarn dev:server |
@@ -1,2 +1,41 @@ | |||||
# pdf-extract-Via_Software | |||||
<h1 align="center">Extract data from PDF</h1> | |||||
# Usando o template | |||||
## Criando um projeto novo | |||||
1. Acesse a pagina do gitea: https://gitea.tron.com.br/Tron_Informatica/pdf-extract-template | |||||
2. Localize o botão "Usar este modelo" <br> | |||||
![Pagina do gitea - Projeto](https://gitea.tron.com.br/Tron_Informatica/pdf-extract-template/raw/branch/master/resources/gitea-template.png) <br> | |||||
3. Preencha os campos e crie o repositorio. Marque a opção "Conteúdo Git (Branch padrão)" e lembre de trocar o proprietario para 'Tron_informatica' <br> | |||||
![Pagina do gitea - Novo repositorio](https://gitea.tron.com.br/Tron_Informatica/pdf-extract-template/raw/branch/master/resources/gitea-novo-repo.png) <br> | |||||
4. clone o novo repositório criado | |||||
## Usando o template | |||||
1. Troque o nome do projeto nos arquivos: docker-composer.yaml e package.json <br> | |||||
![Arquivos para renomear](https://gitea.tron.com.br/Tron_Informatica/pdf-extract-template/raw/branch/master/resources/renomear-projeto.png) <br> | |||||
2. Crie seus controllers, services e rotas, dexei alguns arquivos como teste <br> | |||||
![Arquivos para renomear](https://gitea.tron.com.br/Tron_Informatica/pdf-extract-template/raw/branch/master/resources/arquivos-exemplos.png) <br> | |||||
## Scripts | |||||
### Rodando projeto | |||||
```console | |||||
:~$ yarn docker:server | |||||
``` | |||||
### Exibindo o bash do container | |||||
Obs: o projeto precisa está em execução | |||||
```console | |||||
:~$ yarn docker:bash | |||||
``` | |||||
### Rodando teste unitários | |||||
```console | |||||
:~$ yarn docker:test | |||||
``` | |||||
### Instalando dependência | |||||
```console | |||||
:~$ yarn docker:bash | |||||
root@947961d6b834:/app# yarn add <dependência> | |||||
``` |
@@ -0,0 +1,19 @@ | |||||
module.exports = { | |||||
presets: [ | |||||
['@babel/preset-env', { targets: { node: 'current' } }], | |||||
'@babel/preset-typescript', | |||||
['const-enum', { "transform": "constObject" }] | |||||
], | |||||
plugins: [ | |||||
['module-resolver', { | |||||
alias: { | |||||
'@modules': './src/modules', | |||||
'@config': './src/config', | |||||
'@shared': './src/shared' | |||||
} | |||||
}], | |||||
'babel-plugin-transform-typescript-metadata', | |||||
['@babel/plugin-proposal-decorators', { 'legacy': true }], | |||||
['@babel/plugin-proposal-class-properties', { 'loose': true }] | |||||
], | |||||
} |
@@ -0,0 +1,17 @@ | |||||
version: "3" | |||||
services: | |||||
node: | |||||
build: | |||||
context: ./ | |||||
dockerfile: Dockerfile.development | |||||
container_name: nome-do-projeto | |||||
tty: true | |||||
volumes: | |||||
- ./:/app | |||||
ports: | |||||
- "3333:3333" | |||||
networks: | |||||
- nome-do-projeto | |||||
networks: | |||||
nome-do-projeto: | |||||
driver: bridge |
@@ -0,0 +1,193 @@ | |||||
// For a detailed explanation regarding each configuration property, visit: | |||||
// https://jestjs.io/docs/en/configuration.html | |||||
module.exports = { | |||||
// All imported modules in your tests should be mocked automatically | |||||
// automock: false, | |||||
// Stop running tests after `n` failures | |||||
// bail: 0, | |||||
// The directory where Jest should store its cached dependency information | |||||
// cacheDirectory: "C:\\Users\\danil\\AppData\\Local\\Temp\\jest", | |||||
// Automatically clear mock calls and instances between every test | |||||
clearMocks: true, | |||||
// Indicates whether the coverage information should be collected while executing the test | |||||
collectCoverage: true, | |||||
// An array of glob patterns indicating a set of files for which coverage information should be collected | |||||
collectCoverageFrom: [ | |||||
'<rootDir>/src/services/**/*.ts', | |||||
], | |||||
// The directory where Jest should output its coverage files | |||||
coverageDirectory: 'coverage', | |||||
// An array of regexp pattern strings used to skip coverage collection | |||||
// coveragePathIgnorePatterns: [ | |||||
// "\\\\node_modules\\\\" | |||||
// ], | |||||
// Indicates which provider should be used to instrument code for coverage | |||||
// coverageProvider: "babel", | |||||
// A list of reporter names that Jest uses when writing coverage reports | |||||
coverageReporters: [ | |||||
"text-summary", | |||||
"lcov", | |||||
], | |||||
// An object that configures minimum threshold enforcement for coverage results | |||||
// coverageThreshold: undefined, | |||||
// A path to a custom dependency extractor | |||||
// dependencyExtractor: undefined, | |||||
// Make calling deprecated APIs throw helpful error messages | |||||
// errorOnDeprecated: false, | |||||
// Force coverage collection from ignored files using an array of glob patterns | |||||
// forceCoverageMatch: [], | |||||
// A path to a module which exports an async function that is triggered once before all test suites | |||||
// globalSetup: undefined, | |||||
// A path to a module which exports an async function that is triggered once after all test suites | |||||
// globalTeardown: undefined, | |||||
// A set of global variables that need to be available in all test environments | |||||
// globals: {}, | |||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. | |||||
// maxWorkers: "50%", | |||||
// An array of directory names to be searched recursively up from the requiring module's location | |||||
// moduleDirectories: [ | |||||
// "node_modules" | |||||
// ], | |||||
// An array of file extensions your modules use | |||||
// moduleFileExtensions: [ | |||||
// "js", | |||||
// "json", | |||||
// "jsx", | |||||
// "ts", | |||||
// "tsx", | |||||
// "node" | |||||
// ], | |||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module | |||||
//moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {prefix: '<rootDir>/src/'}), | |||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader | |||||
// modulePathIgnorePatterns: [], | |||||
// Activates notifications for test results | |||||
// notify: false, | |||||
// An enum that specifies notification mode. Requires { notify: true } | |||||
// notifyMode: "failure-change", | |||||
// A preset that is used as a base for Jest's configuration | |||||
preset: 'ts-jest', | |||||
// Run tests from one or more projects | |||||
// projects: undefined, | |||||
// Use this configuration option to add custom reporters to Jest | |||||
// reporters: undefined, | |||||
// Automatically reset mock state between every test | |||||
// resetMocks: false, | |||||
// Reset the module registry before running each individual test | |||||
// resetModules: false, | |||||
// A path to a custom resolver | |||||
// resolver: undefined, | |||||
// Automatically restore mock state between every test | |||||
// restoreMocks: false, | |||||
// The root directory that Jest should scan for tests and modules within | |||||
// rootDir: './src', | |||||
// A list of paths to directories that Jest should use to search for files in | |||||
// roots: [ | |||||
// "<rootDir>" | |||||
// ], | |||||
// Allows you to use a custom runner instead of Jest's default test runner | |||||
// runner: "jest-runner", | |||||
// The paths to modules that run some code to configure or set up the testing environment before each test | |||||
// setupFiles: [], | |||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test | |||||
setupFilesAfterEnv: [ | |||||
"./jest.setup.ts" | |||||
], | |||||
// The number of seconds after which a test is considered as slow and reported as such in the results. | |||||
// slowTestThreshold: 5, | |||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing | |||||
// snapshotSerializers: [], | |||||
// The test environment that will be used for testing | |||||
testEnvironment: "node", | |||||
// Options that will be passed to the testEnvironment | |||||
// testEnvironmentOptions: {}, | |||||
// Adds a location field to test results | |||||
// testLocationInResults: false, | |||||
// The glob patterns Jest uses to detect test files | |||||
testMatch: [ | |||||
"**/*.spec.ts", | |||||
], | |||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped | |||||
// testPathIgnorePatterns: [ | |||||
// "\\\\node_modules\\\\" | |||||
// ], | |||||
// The regexp pattern or array of patterns that Jest uses to detect test files | |||||
// testRegex: [], | |||||
// This option allows the use of a custom results processor | |||||
// testResultsProcessor: undefined, | |||||
// This option allows use of a custom test runner | |||||
// testRunner: "jasmine2", | |||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href | |||||
// testURL: "http://localhost", | |||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" | |||||
// timers: "real", | |||||
// A map from regular expressions to paths to transformers | |||||
// transform: undefined, | |||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation | |||||
// transformIgnorePatterns: [ | |||||
// "\\\\node_modules\\\\", | |||||
// "\\.pnp\\.[^\\\\]+$" | |||||
// ], | |||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them | |||||
// unmockedModulePathPatterns: undefined, | |||||
// Indicates whether each individual test should be reported during the run | |||||
// verbose: undefined, | |||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode | |||||
// watchPathIgnorePatterns: [], | |||||
// Whether to use watchman for file crawling | |||||
// watchman: true, | |||||
}; |
@@ -0,0 +1,2 @@ | |||||
import 'reflect-metadata'; | |||||
import './src/containers'; |
@@ -0,0 +1,13 @@ | |||||
{ | |||||
"type": "mongodb", | |||||
"host": "mongo", | |||||
"port": 27017, | |||||
"username": "", | |||||
"password": "", | |||||
"database": "db-extract-pdf", | |||||
"synchronize": false, | |||||
"logging": false, | |||||
"entities": [ | |||||
"src/modules/**/typeorm/entities/*.ts" | |||||
] | |||||
} |
@@ -0,0 +1,72 @@ | |||||
{ | |||||
"name": "nome-do-projeto", | |||||
"version": "1.0.0", | |||||
"description": "Extraçao de dados de arquivos PDF", | |||||
"main": "server.js", | |||||
"author": "Daniel Souza <daniel.souza@tron.com.br>", | |||||
"license": "MIT", | |||||
"private": true, | |||||
"scripts": { | |||||
"build": "rm -rf ./dist && babel src --extensions \".js,.ts\" --out-dir dist --copy-files", | |||||
"start": "NODE_ENV=production node ./src/http/server.ts", | |||||
"dev:server": "cross-env NODE_ENV=development ts-node-dev -r tsconfig-paths/register --inspect --respawn --transpile-only --ignore-watch node_modules ./src/http/server.ts", | |||||
"docker:run": "docker-compose -f ./docker-compose.yaml down && docker-compose -f ./docker-compose.yaml up --build -d", | |||||
"docker:logs": "docker logs -f nome-do-projeto", | |||||
"docker:server": "yarn docker:run && yarn docker:logs", | |||||
"docker:bash": "docker exec -it nome-do-projeto bash", | |||||
"docker:test": "docker-compose -f ./docker-compose.yaml up --build -d && docker exec -it nome-do-projeto yarn test", | |||||
"test": "jest" | |||||
}, | |||||
"dependencies": { | |||||
"@types/mongodb": "^3.6.0", | |||||
"axios": "^0.20.0", | |||||
"cors": "^2.8.5", | |||||
"date-fns": "^2.16.1", | |||||
"dotenv": "^8.2.0", | |||||
"express": "^4.17.1", | |||||
"express-async-errors": "^3.1.1", | |||||
"mongodb": "^3.6.3", | |||||
"multer": "^1.4.2", | |||||
"pdf-parse": "^1.1.1", | |||||
"reflect-metadata": "^0.1.13", | |||||
"tsyringe": "^4.3.0", | |||||
"typeorm": "^0.2.29", | |||||
"uuid": "^8.3.1", | |||||
"yup": "^0.31.1" | |||||
}, | |||||
"devDependencies": { | |||||
"@babel/cli": "^7.12.1", | |||||
"@babel/core": "^7.12.3", | |||||
"@babel/node": "^7.12.1", | |||||
"@babel/plugin-proposal-class-properties": "^7.12.1", | |||||
"@babel/plugin-proposal-decorators": "^7.12.1", | |||||
"@babel/plugin-transform-typescript": "^7.12.1", | |||||
"@babel/preset-env": "^7.12.1", | |||||
"@babel/preset-typescript": "^7.12.1", | |||||
"@types/cors": "^2.8.7", | |||||
"@types/express": "^4.17.8", | |||||
"@types/jest": "^26.0.19", | |||||
"@types/multer": "^1.4.4", | |||||
"@types/pdf-parse": "^1.1.0", | |||||
"@types/pdfjs-dist": "^2.1.7", | |||||
"@types/uuid": "^8.3.0", | |||||
"@types/yup": "^0.29.9", | |||||
"@typescript-eslint/eslint-plugin": "^4.4.0", | |||||
"@typescript-eslint/parser": "^4.4.0", | |||||
"babel-plugin-module-resolver": "^4.0.0", | |||||
"babel-plugin-transform-typescript-metadata": "^0.3.1", | |||||
"babel-preset-const-enum": "^1.0.0", | |||||
"cross-env": "^7.0.2", | |||||
"eslint": "6.8.0", | |||||
"eslint-config-airbnb-base": "^14.2.0", | |||||
"eslint-config-prettier": "^6.11.0", | |||||
"eslint-import-resolver-typescript": "^2.2.1", | |||||
"eslint-plugin-import": "^2.21.2", | |||||
"eslint-plugin-prettier": "^3.1.4", | |||||
"jest": "^26.6.3", | |||||
"prettier": "^2.1.0", | |||||
"ts-jest": "^26.4.4", | |||||
"ts-node-dev": "^1.0.0-pre.63", | |||||
"typescript": "^4.0.3" | |||||
} | |||||
} |
@@ -0,0 +1,6 @@ | |||||
module.exports = { | |||||
singleQuote: true, | |||||
trailingComma: 'all', | |||||
arrowParens: 'avoid', | |||||
printWidth: 100, | |||||
} |
@@ -0,0 +1,224 @@ | |||||
[ | |||||
{ | |||||
"company": { | |||||
"name": "INSTITUTO DE EDUCACAO VOOS OLYMPIO LTDA", | |||||
"code": "00261", | |||||
"cnpj": "31001804000139", | |||||
"refEndDate": "2020-10-31", | |||||
"refStartDate": "2020-10-01", | |||||
"address": "COND SOLAR DE BRASILIA QD 02 BL B, 207 LOTES 3 E 4" | |||||
}, | |||||
"employee": { | |||||
"code": "000001", | |||||
"name": "RAPHAEL VOOS LENZI", | |||||
"salary": 3000, | |||||
"role": "SUPERVISOR PEDAGOGICO", | |||||
"admissionDate": "01/09/2018", | |||||
"baseINSS": 3000, | |||||
"aliquotINSS": 9.3873, | |||||
"baseFGTS": 3000, | |||||
"valueFGTS": 240, | |||||
"baseIRRF": 2718.38, | |||||
"events": [ | |||||
{ | |||||
"code": "001", | |||||
"description": "Salário Base", | |||||
"discount": 0, | |||||
"eventReference": "", | |||||
"earnings": 3000 | |||||
}, | |||||
{ | |||||
"code": "615", | |||||
"description": "TAXA ASSISTENCIAL 2%", | |||||
"discount": 60, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "903", | |||||
"description": "INSS Folha", | |||||
"discount": 281.62, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "914", | |||||
"description": "IRRF Folha", | |||||
"discount": 61.08, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
} | |||||
] | |||||
} | |||||
}, | |||||
{ | |||||
"company": { | |||||
"name": "INSTITUTO DE EDUCACAO VOOS OLYMPIO LTDA", | |||||
"code": "00261", | |||||
"cnpj": "31001804000139", | |||||
"refEndDate": "2020-10-31", | |||||
"refStartDate": "2020-10-01", | |||||
"address": "COND SOLAR DE BRASILIA QD 02 BL B, 207 LOTES 3 E 4" | |||||
}, | |||||
"employee": { | |||||
"code": "000005", | |||||
"name": "MARIA BÁRBARA SEIXO DE BRITTO DE MELO", | |||||
"salary": 1536.9, | |||||
"role": "ASSISTENTE ADMINISTRATIVO", | |||||
"admissionDate": "02/01/2019", | |||||
"baseINSS": 640.37, | |||||
"aliquotINSS": 7.5, | |||||
"baseFGTS": 640.37, | |||||
"valueFGTS": 51.22, | |||||
"baseIRRF": 592.35, | |||||
"events": [ | |||||
{ | |||||
"code": "001", | |||||
"description": "Salário Base", | |||||
"discount": 0, | |||||
"eventReference": "", | |||||
"earnings": 640.37 | |||||
}, | |||||
{ | |||||
"code": "637", | |||||
"description": "PLANO DE SAÚDE", | |||||
"discount": 202.51, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "604", | |||||
"description": "Vale Transporte 6%", | |||||
"discount": 38.42, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "615", | |||||
"description": "TAXA ASSISTENCIAL 2%", | |||||
"discount": 30.72, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "903", | |||||
"description": "INSS Folha", | |||||
"discount": 48.02, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
} | |||||
] | |||||
} | |||||
}, | |||||
{ | |||||
"company": { | |||||
"name": "INSTITUTO DE EDUCACAO VOOS OLYMPIO LTDA", | |||||
"code": "00261", | |||||
"cnpj": "31001804000139", | |||||
"refEndDate": "2020-10-31", | |||||
"refStartDate": "2020-10-01", | |||||
"address": "COND SOLAR DE BRASILIA QD 02 BL B, 207 LOTES 3 E 4" | |||||
}, | |||||
"employee": { | |||||
"code": "000011", | |||||
"name": "VALDIRA PEREIRA", | |||||
"salary": 1229.52, | |||||
"role": "AUXILIAR DE SERVIÇOS GERAIS", | |||||
"admissionDate": "16/10/2019", | |||||
"baseINSS": 512.29, | |||||
"aliquotINSS": 7.5, | |||||
"baseFGTS": 512.29, | |||||
"valueFGTS": 40.98, | |||||
"baseIRRF": 284.28, | |||||
"events": [ | |||||
{ | |||||
"code": "001", | |||||
"description": "Salário Base", | |||||
"discount": 0, | |||||
"eventReference": "", | |||||
"earnings": 512.29 | |||||
}, | |||||
{ | |||||
"code": "599", | |||||
"description": "Salário Família", | |||||
"discount": 0, | |||||
"eventReference": "001,00", | |||||
"earnings": 48.62 | |||||
}, | |||||
{ | |||||
"code": "604", | |||||
"description": "Vale Transporte 6%", | |||||
"discount": 30.73, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "615", | |||||
"description": "TAXA ASSISTENCIAL 2%", | |||||
"discount": 24.6, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "903", | |||||
"description": "INSS Folha", | |||||
"discount": 38.42, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
} | |||||
] | |||||
} | |||||
}, | |||||
{ | |||||
"company": { | |||||
"name": "INSTITUTO DE EDUCACAO VOOS OLYMPIO LTDA", | |||||
"code": "00261", | |||||
"cnpj": "31001804000139", | |||||
"refEndDate": "2020-10-31", | |||||
"refStartDate": "2020-10-01", | |||||
"address": "COND SOLAR DE BRASILIA QD 02 BL B, 207 LOTES 3 E 4" | |||||
}, | |||||
"employee": { | |||||
"code": "000012", | |||||
"name": "BRUNO LINS VOOS", | |||||
"salary": 1536.9, | |||||
"role": "CONSULTOR DE VENDAS", | |||||
"admissionDate": "13/01/2020", | |||||
"baseINSS": 640.37, | |||||
"aliquotINSS": 7.5, | |||||
"baseFGTS": 640.37, | |||||
"valueFGTS": 51.22, | |||||
"baseIRRF": 592.35, | |||||
"events": [ | |||||
{ | |||||
"code": "001", | |||||
"description": "Salário Base", | |||||
"discount": 0, | |||||
"eventReference": "", | |||||
"earnings": 640.37 | |||||
}, | |||||
{ | |||||
"code": "604", | |||||
"description": "Vale Transporte 6%", | |||||
"discount": 38.42, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "615", | |||||
"description": "TAXA ASSISTENCIAL 2%", | |||||
"discount": 30.72, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
}, | |||||
{ | |||||
"code": "903", | |||||
"description": "INSS Folha", | |||||
"discount": 48.02, | |||||
"eventReference": "", | |||||
"earnings": 0 | |||||
} | |||||
] | |||||
} | |||||
} | |||||
] |
@@ -0,0 +1,107 @@ | |||||
import { container } from 'tsyringe'; | |||||
import path from 'path'; | |||||
import { promises as fs } from 'fs'; | |||||
import { Readable } from 'stream'; | |||||
import AppError from '../../../erros/AppError'; | |||||
import ExtractDataPDFMapaFolhaService from '../../../services/mapaFolha/ExtractDataPDFMapaFolhaService'; | |||||
import reciboPagamentoData from '../../resources/recibo-pagamento-data.json'; | |||||
describe('Mapa Folha - ExtractDataPDFMapaFolhaService', () => { | |||||
afterEach(() => { | |||||
jest.restoreAllMocks(); | |||||
}); | |||||
it('Deve ser capaz de extrair os dados do PDF do mapa da folha ', async () => { | |||||
const extractDataPDFMapaFolha = container.resolve(ExtractDataPDFMapaFolhaService); | |||||
const filePath = path.resolve(__dirname, '..', '..', 'resources', 'recibo-pagamento.pdf'); | |||||
const file = await fs.readFile(filePath); | |||||
const { size } = await fs.stat(filePath); | |||||
const readableStream = new Readable(); | |||||
const fileMulter: Express.Multer.File = { | |||||
fieldname: 'pdf', | |||||
originalname: 'report-mapa-folha.pdf', | |||||
encoding: 'utf-8', | |||||
mimetype: 'application/pdf', | |||||
size, | |||||
stream: readableStream, | |||||
destination: filePath, | |||||
filename: 'report-mapa-folha.pdf', | |||||
path: filePath, | |||||
buffer: file, | |||||
}; | |||||
const data = await extractDataPDFMapaFolha.execute({ | |||||
files: [fileMulter], | |||||
}); | |||||
expect(data).toEqual(expect.arrayContaining(reciboPagamentoData)); | |||||
}); | |||||
it('Deve gerar uma exceção caso não seja passado o arquivo ', async () => { | |||||
const params = {} as { files: Express.Multer.File[] }; | |||||
const extractDataPDFMapaFolha = container.resolve(ExtractDataPDFMapaFolhaService); | |||||
await expect(extractDataPDFMapaFolha.execute(params)).rejects.toBeInstanceOf(AppError); | |||||
}); | |||||
it('Deve gerar uma exceção caso o arquivo não seja um PDF 1', async () => { | |||||
const extractDataPDFMapaFolha = container.resolve(ExtractDataPDFMapaFolhaService); | |||||
const filePath = path.resolve(__dirname, '..', '..', 'resources', 'recibo-pagamento-data.json'); | |||||
const file = await fs.readFile(filePath); | |||||
const { size } = await fs.stat(filePath); | |||||
const readableStream = new Readable(); | |||||
const fileMulter: Express.Multer.File = { | |||||
fieldname: 'pdf', | |||||
originalname: 'recibo-pagamento-data.json', | |||||
encoding: 'utf-8', | |||||
mimetype: 'application/json', | |||||
size, | |||||
stream: readableStream, | |||||
destination: filePath, | |||||
filename: 'recibo-pagamento-data.json', | |||||
path: filePath, | |||||
buffer: file, | |||||
}; | |||||
await expect(extractDataPDFMapaFolha.execute({ files: [fileMulter] })).rejects.toBeInstanceOf( | |||||
AppError, | |||||
); | |||||
}); | |||||
it('Não deve gerar uma exceção caso receba um PDF diferente do experado ', async () => { | |||||
const extractDataPDFMapaFolha = container.resolve(ExtractDataPDFMapaFolhaService); | |||||
const filePath = path.resolve(__dirname, '..', '..', 'resources', 'empty-pdf.pdf'); | |||||
const file = await fs.readFile(filePath); | |||||
const { size } = await fs.stat(filePath); | |||||
const readableStream = new Readable(); | |||||
const fileMulter: Express.Multer.File = { | |||||
fieldname: 'pdf', | |||||
originalname: 'empty-pdf.pdf', | |||||
encoding: 'utf-8', | |||||
mimetype: 'application/pdf', | |||||
size, | |||||
stream: readableStream, | |||||
destination: filePath, | |||||
filename: 'empty-pdf.pdf', | |||||
path: filePath, | |||||
buffer: file, | |||||
}; | |||||
const data = await extractDataPDFMapaFolha.execute({ | |||||
files: [fileMulter], | |||||
}); | |||||
expect(data).toEqual([]); | |||||
}); | |||||
}); |
@@ -0,0 +1,22 @@ | |||||
import multer from 'multer'; | |||||
import crypto from 'crypto'; | |||||
import os from 'os'; | |||||
const tmpFolder = os.tmpdir(); | |||||
export default { | |||||
tempdir: { | |||||
directory: tmpFolder, | |||||
storage: multer.diskStorage({ | |||||
destination: tmpFolder, | |||||
filename(request, file, callback) { | |||||
const fileHash = crypto.randomBytes(10).toString('hex'); | |||||
const originalname = file.originalname.replace(/[^0-9-a-zA-Z.]/g, '-'); | |||||
const filename = `${fileHash}-${originalname}`; | |||||
return callback(null, filename); | |||||
}, | |||||
}), | |||||
}, | |||||
}; |
@@ -0,0 +1,5 @@ | |||||
import { container } from 'tsyringe'; | |||||
import IPdfToTextProvider from './providers/pdfToText/IPdfToTextProvider'; | |||||
import LinuxPdfToText from './providers/pdfToText/implementations/LinuxPdfToText'; | |||||
container.registerSingleton<IPdfToTextProvider>('PdfToTextProvider', LinuxPdfToText); |
@@ -0,0 +1,5 @@ | |||||
import IOptionsPdfParseDTO from './dto/IOptionsPdfParseDTO'; | |||||
export default interface IPdfToTextProvider { | |||||
extract(filePath: string, options?: IOptionsPdfParseDTO): Promise<string>; | |||||
} |
@@ -0,0 +1,3 @@ | |||||
export default interface IOptionsPdfParseDTO { | |||||
pageNumber?: number; | |||||
} |
@@ -0,0 +1,32 @@ | |||||
import fs from 'fs'; | |||||
import { v4 } from 'uuid'; | |||||
import os from 'os'; | |||||
import { exec } from 'child_process'; | |||||
import path from 'path'; | |||||
import IPdfToTextProvider from '../IPdfToTextProvider'; | |||||
import IOptionsPdfParseDTO from '../dto/IOptionsPdfParseDTO'; | |||||
export default class LinuxPdfToText implements IPdfToTextProvider { | |||||
public async extract(filePath: string, options: IOptionsPdfParseDTO = {}): Promise<string> { | |||||
const tempTextFile = path.resolve(os.tmpdir(), v4()); | |||||
const { pageNumber } = options; | |||||
const pageNumberOption = pageNumber !== undefined ? `-f ${pageNumber} -l ${pageNumber}` : ''; | |||||
const command = `pdftotext -layout ${pageNumberOption} ${filePath} ${tempTextFile}`; | |||||
return new Promise((resolve, reject) => { | |||||
exec(command, async err => { | |||||
if (err) { | |||||
return reject(err); | |||||
} | |||||
const contentFile = await fs.promises.readFile(tempTextFile); | |||||
await fs.promises.unlink(tempTextFile); | |||||
return resolve(contentFile.toString()); | |||||
}); | |||||
}); | |||||
} | |||||
} |
@@ -0,0 +1,13 @@ | |||||
import pdfParse from 'pdf-parse'; | |||||
import fs from 'fs'; | |||||
import IPdfToTextProvider from '../IPdfToTextProvider'; | |||||
export default class PdfParseProvider implements IPdfToTextProvider { | |||||
public async extract(filePath: string): Promise<string> { | |||||
const dataBuffer = fs.readFileSync(filePath); | |||||
const data = await pdfParse(dataBuffer); | |||||
return data.text; | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
export default class AppError { | |||||
public readonly message: string; | |||||
public readonly statusCode: number; | |||||
public constructor(message: string, statusCode = 400) { | |||||
this.message = message; | |||||
this.statusCode = statusCode; | |||||
} | |||||
} |
@@ -0,0 +1,15 @@ | |||||
import { Request, Response } from 'express'; | |||||
import { container } from 'tsyringe'; | |||||
import ExempleService from '../../services/exemple/ExempleService'; | |||||
export default class ExempleController { | |||||
public async exemple(request: Request, response: Response): Promise<Response> { | |||||
const files = Array.isArray(request.files) ? request.files : request.files.pdf; | |||||
const exemple = container.resolve(ExempleService); | |||||
const responseData = await exemple.execute({ files }); | |||||
return response.json(responseData); | |||||
} | |||||
} |
@@ -0,0 +1,23 @@ | |||||
import { Response, Request, NextFunction } from 'express'; | |||||
import AppError from '../../erros/AppError'; | |||||
export default function error( | |||||
err: Error, | |||||
_request: Request, | |||||
response: Response, | |||||
_next: NextFunction, | |||||
): Response { | |||||
if (err instanceof AppError) { | |||||
return response.status(err.statusCode).json({ | |||||
status: 'error', | |||||
message: err.message, | |||||
}); | |||||
} | |||||
console.error(err); | |||||
return response.status(500).json({ | |||||
status: 'error', | |||||
message: 'Internal server error', | |||||
}); | |||||
} |
@@ -0,0 +1,12 @@ | |||||
import { Router } from 'express'; | |||||
import multer from 'multer'; | |||||
import uploadConfig from '../../config/upload'; | |||||
import ExempleController from '../controllers/ExempleController'; | |||||
const exempleRoutes = Router(); | |||||
const exempleController = new ExempleController(); | |||||
const upload = multer(uploadConfig.tempdir); | |||||
exempleRoutes.post('/exemple', upload.array('pdf'), exempleController.exemple); | |||||
export default exempleRoutes; |
@@ -0,0 +1,8 @@ | |||||
import { Router } from 'express'; | |||||
import exempleRoutes from './exemple.routes'; | |||||
const routes = Router(); | |||||
routes.use('/exemple', exempleRoutes); | |||||
export default routes; |
@@ -0,0 +1,22 @@ | |||||
import 'reflect-metadata'; | |||||
import 'express-async-errors'; | |||||
import 'dotenv/config'; | |||||
import '../containers'; | |||||
import express from 'express'; | |||||
import cors from 'cors'; | |||||
import net from 'net'; | |||||
import error from './middlewares/error'; | |||||
import routes from './routes'; | |||||
const app = express(); | |||||
app.use(cors()); | |||||
app.use(express.json({ limit: '50MB' })); | |||||
app.use(routes); | |||||
app.use(error); | |||||
const listener = app.listen(3333, () => { | |||||
const { port } = listener.address() as net.AddressInfo; | |||||
console.log(`Server started on port ${port}`); | |||||
}); |
@@ -0,0 +1,58 @@ | |||||
import { inject, injectable } from 'tsyringe'; | |||||
import AppError from '../../erros/AppError'; | |||||
import IPdfToTextProvider from '../../containers/providers/pdfToText/IPdfToTextProvider'; | |||||
import regexMapaFolha from '../../utils/regex/regexMapaFolha'; | |||||
import * as format from '../../utils/format'; | |||||
interface IRequest { | |||||
files: Express.Multer.File[]; | |||||
} | |||||
interface ICompanyContent { | |||||
name: string; | |||||
code: string; | |||||
cnpj: string; | |||||
refEndDate: string; | |||||
refStartDate: string; | |||||
address: string; | |||||
contentEmployee: string; | |||||
} | |||||
type IResponse = Array<{ | |||||
fileName: string; | |||||
content: string; | |||||
}>; | |||||
@injectable() | |||||
export default class ExempleService { | |||||
constructor( | |||||
@inject('PdfToTextProvider') | |||||
private pdfToTextProvider: IPdfToTextProvider, | |||||
) {} | |||||
public async execute({ files }: IRequest): Promise<IResponse> { | |||||
if (!files) { | |||||
throw new AppError('File is required'); | |||||
} | |||||
files.forEach(file => { | |||||
if (file.mimetype !== 'application/pdf') { | |||||
throw new AppError('Only PDF is accepted'); | |||||
} | |||||
}); | |||||
const response: IResponse = []; | |||||
for (let indexFile = 0; indexFile < files.length; indexFile++) { | |||||
const file = files[indexFile]; | |||||
const contentFile = await this.pdfToTextProvider.extract(file.path); | |||||
response.push({ | |||||
fileName: file.filename, | |||||
content: contentFile, | |||||
}); | |||||
} | |||||
return response; | |||||
} | |||||
} |
@@ -0,0 +1,16 @@ | |||||
const dates = [ | |||||
{ description: 'janeiro', value: 1 }, | |||||
{ description: 'fevereiro', value: 2 }, | |||||
{ description: 'março', value: 3 }, | |||||
{ description: 'abril', value: 4 }, | |||||
{ description: 'maio', value: 5 }, | |||||
{ description: 'junho', value: 6 }, | |||||
{ description: 'julho', value: 7 }, | |||||
{ description: 'agosto', value: 8 }, | |||||
{ description: 'setembro', value: 9 }, | |||||
{ description: 'outubro', value: 10 }, | |||||
{ description: 'novembro', value: 11 }, | |||||
{ description: 'dezembro', value: 12 }, | |||||
]; | |||||
export default dates; |
@@ -0,0 +1,33 @@ | |||||
import dates from '../date/dates'; | |||||
export const registryRemoveMask = (cnpj: string): string => cnpj?.replace(/\D/gm, '') || ''; | |||||
export const competenceDescriptionToDate = (competenceDescription: string): string => { | |||||
const [monthDescription, year] = competenceDescription.split('de'); | |||||
const month = dates.find(date => { | |||||
const dateDescriptionNormalized = date.description.toLocaleLowerCase('en-US').trim(); | |||||
const monthDescriptionNormalized = monthDescription.toLocaleLowerCase('en-US').trim(); | |||||
return dateDescriptionNormalized === monthDescriptionNormalized; | |||||
})?.value; | |||||
const monthFormatted = `00${month}`.slice(-2); | |||||
return `${monthFormatted}/${year.trim()}`; | |||||
}; | |||||
export const brazilianDateToEnglish = (brazilianDate: string): string => { | |||||
const date = brazilianDate || ''; | |||||
const [day, month, year] = date.split('/'); | |||||
return day && month && year ? `${year}-${month}-${day}` : ''; | |||||
}; | |||||
export const brazilianMoneyToNumber = (money: string): number => { | |||||
const value = money?.replace(/\./gm, '').replace(/,/, '.'); | |||||
return value ? Number(value) : 0; | |||||
}; | |||||
export default {}; |
@@ -0,0 +1,39 @@ | |||||
const regexMapaFolha = { | |||||
companyName: (): RegExp => /Empresa\s*:\s*(?<companyName>.*?)\s*\(\s*(\d{1,})\s*\)\s*Página/gm, | |||||
companyCode: (): RegExp => /Empresa\s*:\s*.*?\s*\(\s*(?<companyCode>\d{1,})\s*\)\s*Página/gm, | |||||
companyAddress: (): RegExp => /End\.\s*:\s*(?<companyAddress>.*?)CNPJ\/CEI/gm, | |||||
/** | |||||
* Em caso de endereços grande pode quebrar o CNPJ por isso estou pegando qualquer ocorrencia de 14 caracteres numericos entre "CNPJ/CEI" e "Ref" | |||||
* até o momento não teve caso de partir o cnpj no meio | |||||
*/ | |||||
companyCNPJ: (): RegExp => /CNPJ\/CEI:((.|\n)*?)(?<companyCNPJ>\d{14})((.|\n)*?)Ref\.:/gm, | |||||
reportReference: (): RegExp => | |||||
/Ref\.:\s*(?<refStartDate>\d{2}\/\d{2}\/\d{4})\s*a\s*(?<refEndDate>\d{2}\/\d{2}\/\d{4})/gm, | |||||
contentEmployePage: (): RegExp => | |||||
/Ref\.:\s*\d{2}\/\d{2}\/\d{4}\s*a\s*\d{2}\/\d{2}\/\d{4}(.*?)\n{1,}(?<contentEmployePage>(.|\n)*)/gm, | |||||
employeeInfo: (): RegExp => | |||||
/^(?<employeeCode>\d{1,})\s*(?<employeeName>.*?)(?<employeeSalary>(\.?\d{1,}){1,},\d{2})\s*Função\s*:(?<employeeRole>(.|\n)*?)(?<employeeContentRest>Livro(.|\n)*?Base INSS:.*$)/gm, | |||||
employeeAdmissionDate: (): RegExp => | |||||
/Admissão\s*:\s*(?<employeeAdmissionDate>\d{2}\/\d{2}\/\d{4})/gm, | |||||
employeeEvents: (): RegExp => /Admissão\s*:(.*?)\n(?<events>(.|\n)*?)(.*)\n\s*\*{5,}/gm, | |||||
eventReference: (): RegExp => /.*?\s{2,}(?<eventReference>.*?)\s{2,}.*/g, | |||||
eventInfo: (): RegExp => /^(?<eventCode>\d{1,})\s*(?<eventDescription>(.*?))\s{2}/g, | |||||
inss: (): RegExp => /Base\sINSS:(?<baseINSS>.*?)\(Aliq\.:(?<aliquotINSS>(.*?))%\)/gm, | |||||
fgts: (): RegExp => /Base\sFGTS:(?<baseFGTS>.*?)\(Valor:(?<valueFGTS>(.*?))\)/gm, | |||||
baseIRRF: (): RegExp => /Base\sIRRF\sFolha:\s(?<baseIRRF>.{1,15})/gm, | |||||
}; | |||||
export default regexMapaFolha; |
@@ -0,0 +1,16 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
"target": "es5", | |||||
"module": "commonjs", | |||||
"outDir": "./dist", | |||||
"strict": true, | |||||
"strictPropertyInitialization": false, | |||||
"resolveJsonModule": true, | |||||
"esModuleInterop": true, | |||||
"experimentalDecorators": true, | |||||
"emitDecoratorMetadata": true, | |||||
"skipLibCheck": true, | |||||
"forceConsistentCasingInFileNames": true, | |||||
"allowJs": true | |||||
} | |||||
} |