| @@ -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 | |||
| 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 | |||
| *.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> | |||
|  <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> | |||
|  <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> | |||
|  <br> | |||
| 2. Crie seus controllers, services e rotas, dexei alguns arquivos como teste <br> | |||
|  <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 | |||
| } | |||
| } | |||