├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── config.ru ├── lib └── boleto_api.rb └── test_run.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: "0 4 * * 0" 10 | 11 | jobs: 12 | build-boleto-cnab-api: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v3 18 | - name: Login to ghcr.io 19 | uses: docker/login-action@v3 20 | with: 21 | registry: ghcr.io 22 | username: ${{ github.repository_owner }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | - name: Build image 25 | uses: docker/build-push-action@v5 26 | with: 27 | context: . 28 | file: Dockerfile 29 | cache-from: type=registry,ref=ghcr.io/build-boleto-cnab-api-latest 30 | cache-to: type=local,dest=/tmp/.buildx-cache 31 | load: true 32 | - name: Install test pre-requisites 33 | run: pip install pytest 34 | - name: Test 35 | run: pytest -v 36 | - name: Push image 37 | uses: docker/build-push-action@v5 38 | with: 39 | context: . 40 | file: Dockerfile 41 | tags: | 42 | ghcr.io/${{ github.repository }}:latest 43 | cache-from: type=local,src=/tmp/.buildx-cache 44 | cache-to: type=inline 45 | push: true 46 | if: ${{ github.repository_owner == 'akretion' && github.ref == 'refs/heads/master' }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Backup files 2 | *~ 3 | *.swp 4 | 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | LABEL org.opencontainers.image.authors="raphael.valyi@akretion.com" 3 | 4 | WORKDIR /usr/src/app 5 | COPY . . 6 | RUN addgroup -S app && adduser -S -G app app && \ 7 | mkdir -p tmp log && chown app:app tmp log 8 | 9 | RUN set -eux; \ 10 | apk update && \ 11 | apk upgrade && \ 12 | apk add --no-cache \ 13 | build-base \ 14 | ghostscript \ 15 | git \ 16 | ruby-dev \ 17 | && rm -rf /var/cache/apk/* \ 18 | ; 19 | 20 | RUN set -eux; \ 21 | gem install bundler:2.5.11 --no-document \ 22 | && bundle install \ 23 | && rm -rf /usr/local/bundle/cache/*.gem \ 24 | ; 25 | 26 | # throw errors if Gemfile has been modified since Gemfile.lock 27 | RUN bundle config --global frozen 1 && bundle install 28 | 29 | EXPOSE 9292 30 | USER app 31 | CMD ["bundle", "exec", "puma", "config.ru"] 32 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'brcobranca', git: 'https://github.com/kivanio/brcobranca.git' 4 | gem 'grape' 5 | gem 'puma' 6 | gem 'base64' 7 | gem 'mutex_m' 8 | gem 'bigdecimal' 9 | # Erro na versão 0.9.9 https://github.com/shairontoledo/rghost/issues/75 10 | gem 'rghost', git: 'https://github.com/shairontoledo/rghost.git' 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/kivanio/brcobranca.git 3 | revision: a846c103eeefb5d7bc5d7f35c0b493366622425c 4 | specs: 5 | brcobranca (11.1.0) 6 | fast_blank 7 | parseline (>= 1.0.3) 8 | rghost (>= 0.9.8) 9 | rghost_barcode (>= 0.9) 10 | 11 | GIT 12 | remote: https://github.com/shairontoledo/rghost.git 13 | revision: 93240b15141abd9be961ab7b1c68c77fc445e4e7 14 | specs: 15 | rghost (0.9.9) 16 | 17 | GEM 18 | remote: https://rubygems.org/ 19 | specs: 20 | activesupport (7.2.1) 21 | base64 22 | bigdecimal 23 | concurrent-ruby (~> 1.0, >= 1.3.1) 24 | connection_pool (>= 2.2.5) 25 | drb 26 | i18n (>= 1.6, < 2) 27 | logger (>= 1.4.2) 28 | minitest (>= 5.1) 29 | securerandom (>= 0.3) 30 | tzinfo (~> 2.0, >= 2.0.5) 31 | base64 (0.2.0) 32 | bigdecimal (3.1.8) 33 | concurrent-ruby (1.3.4) 34 | connection_pool (2.4.1) 35 | drb (2.2.1) 36 | dry-core (1.0.1) 37 | concurrent-ruby (~> 1.0) 38 | zeitwerk (~> 2.6) 39 | dry-inflector (1.1.0) 40 | dry-logic (1.5.0) 41 | concurrent-ruby (~> 1.0) 42 | dry-core (~> 1.0, < 2) 43 | zeitwerk (~> 2.6) 44 | dry-types (1.7.2) 45 | bigdecimal (~> 3.0) 46 | concurrent-ruby (~> 1.0) 47 | dry-core (~> 1.0) 48 | dry-inflector (~> 1.0) 49 | dry-logic (~> 1.4) 50 | zeitwerk (~> 2.6) 51 | fast_blank (1.0.1) 52 | grape (2.2.0) 53 | activesupport (>= 6) 54 | dry-types (>= 1.1) 55 | mustermann-grape (~> 1.1.0) 56 | rack (>= 2) 57 | zeitwerk 58 | i18n (1.14.6) 59 | concurrent-ruby (~> 1.0) 60 | logger (1.6.1) 61 | minitest (5.25.1) 62 | mustermann (3.0.3) 63 | ruby2_keywords (~> 0.0.1) 64 | mustermann-grape (1.1.0) 65 | mustermann (>= 1.0.0) 66 | mutex_m (0.2.0) 67 | nio4r (2.7.3) 68 | parseline (1.0.3) 69 | puma (6.4.3) 70 | nio4r (~> 2.0) 71 | rack (3.1.7) 72 | rghost_barcode (0.9) 73 | ruby2_keywords (0.0.5) 74 | securerandom (0.3.1) 75 | tzinfo (2.0.6) 76 | concurrent-ruby (~> 1.0) 77 | zeitwerk (2.6.18) 78 | 79 | PLATFORMS 80 | ruby 81 | 82 | DEPENDENCIES 83 | base64 84 | bigdecimal 85 | brcobranca! 86 | grape 87 | mutex_m 88 | puma 89 | rghost! 90 | 91 | BUNDLED WITH 92 | 2.5.11 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Raphaël Valyi - Akretion 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sobre o projeto boleto_cnab_api 2 | 3 | O projeto de gestão de Boletos, Remessas e Retornos Bancários https://github.com/kivanio/brcobranca é muito bem feito, bem testado e mantido. 4 | 5 | É interessante poder usar o projeto BRCobranca (escrito em Ruby) a partir de outras linguagens na forma de um micro-serviço REST. 6 | Mais especificamente, a [Akretion](http://www.akretion.com) que é a empresa que lidera a localização do Odoo no Brasil desde 2009 https://github.com/OCA/l10n-brazil e co-criou a fundação [OCA](https://odoo-community.org/) usa esse projeto para gerenciar Boletos, Remessas e Retornos a partir do ERP Odoo (feito em Python, módulo específico https://github.com/OCA/l10n-brazil/tree/14.0/l10n_br_account_payment_brcobranca). 7 | 8 | A imagem usada no projeto é do OS [Alpine](https://hub.docker.com/_/alpine), o motivo é que por ser um Micro-Serviço quanto menor a imagem melhor e apesar de existir dentro das imagens [Ruby](https://hub.docker.com/_/ruby) tanto a opção Debian quanto Alpine a imagem criada a partir da versão "pura" acaba sendo menor( Ruby-Debian 746MB | Ruby-Alpine 565MB | Alpine 523MB ), existem diferenças entre o [Debian](https://pt.wikipedia.org/wiki/Debian) e o [Alpine](https://pt.wikipedia.org/wiki/Alpine_Linux) basicamente "na superfície" são alguns nomes de pacote e o instalador de pacotes, no Debian apt-get e no Alpine apk, outros comandos Linux são iguais, em caso de algum erro complexo o Debian pode acabar sendo usado. 9 | 10 | # Funcionalidades 11 | 12 | Imprime **Boletos**, gera arquivos de **Remessa** e lê os arquivos de **Retorno** nos formatos CNAB 240, CNAB 400 para os 16 principais bancos do Brasil (Banco do Brasil, Banco do Nordeste, Banestes, Santander, Banrisul, Banco de Brasília, Caixa, Bradesco, Itaú, HSBC, Sicredi, Sicoob, AILOS, Unicred, CREDISIS e Citibank). Mas o grande barato desse projeto é que fazemos isso com menos de 200 linhas de código! Já comparou quantas linhas de de código você tem que manter sozinho ou quase se for re-fazer na linguagem que você quer tudo que o BRCobranca já faz? Seriam dezenas de milhares de linhas e você nunca teria uma qualidade tão boa... 13 | 14 | # API 15 | 16 | ```ruby 17 | # Validar os dados de um Boleto: 18 | GET /boleto/validate 19 | requires :bank, type: String, desc: 'Bank' 20 | requires :data, type: String, desc: 'Boleto data as a stringified json' 21 | 22 | # Obter o nosso_numero de um Boleto: 23 | GET /boleto/nosso_numero 24 | requires :bank, type: String, desc: 'Bank' 25 | requires :data, type: String, desc: 'Boleto data as a stringified json' 26 | 27 | # Imprimir um Boleto apenas: 28 | GET /boleto/get 29 | requires :bank, type: String, desc: 'Bank' 30 | requires :type, type: String, desc: 'Type: pdf|jpg|png|tif' 31 | requires :data, type: String, desc: 'Boleto data as a stringified json' 32 | 33 | # Imprimir uma lista de Boletos: 34 | POST /boleto/multi 35 | requires :type, type: String, desc: 'Type: pdf|jpg|png|tif' 36 | requires :data, type: File, desc: 'json of the list of boletos, including the "bank" key' 37 | 38 | # Gerir um arquivo de Remessa CNAB 240 ou CNAB 400: 39 | POST /remessa 40 | requires :bank, type: String, desc: 'Bank' 41 | requires :type, type: String, desc: 'Type: cnab400|cnab240' 42 | requires :data, type: File, desc: 'json of the list of pagamentos' 43 | 44 | # Transformar um arquivo de Retorno CNAB 240 ou CNAB 400 em JSON: 45 | POST /retorno 46 | requires :bank, type: String, desc: 'Bank' 47 | requires :type, type: String, desc: 'Type: cnab400|cnab240' 48 | requires :data, type: File, desc: 'txt of the retorno file' 49 | ``` 50 | 51 | Nota: os campos datas devem estar no formato YYYY/MM/DD 52 | 53 | O API está documentado com mais detalhes no código aqui: https://github.com/akretion/boleto_cnab_api/blob/master/lib/boleto_api.rb 54 | 55 | # Como rodar o micro-serviço 56 | 57 | ```bash 58 | docker run -p 9292:9292 ghcr.io/akretion/boleto_cnab_api 59 | ``` 60 | 61 | # Exemplos de como consumir o serviço usando sua linguagem preferida: 62 | 63 | ## Bash 64 | 65 | Por exemplo, para imprimir uma lista de Boletos é preciso criar um arquivo temporario com os Boletos em formato JSON e depois fazer um POST do arquivo: 66 | ```bash 67 | echo '[{"valor":5.0,"cedente":"Kivanio Barbosa","documento_cedente":"12345678912","sacado":"Claudio Pozzebom", 68 | "sacado_documento":"12345678900","agencia":"0810","conta_corrente":"53678","convenio":12387,"nosso_numero":"12345678","bank":"itau"}, 69 | {"valor": 10.00,"cedente": "PREFEITURA MUNICIPAL DE VILHENA","documento_cedente": "04092706000181","sacado": "João Paulo Barbosa", 70 | "sacado_documento": "77777777777","agencia": "1825","conta_corrente": "0000528","convenio": "245274","nosso_numero": "000000000000001","bank":"caixa"}]'\ 71 | > /tmp/boletos_data.json 72 | curl -X POST -F type=pdf -F 'data=@/tmp/boletos_data.json' localhost:9292/api/boleto/multi > /tmp/boletos.pdf 73 | ``` 74 | Você pode então conferir os Boletos gerados no arquivo ```/tmp/boletos.pdf``` 75 | 76 | ## Python 77 | 78 | ``` 79 | TODO 80 | ``` 81 | (Ver os exemplos nos módulos Odoo: https://github.com/OCA/l10n-brazil/tree/14.0/l10n_br_account_payment_brcobranca) 82 | 83 | ## Java 84 | 85 | ``` 86 | TODO (contribuições bem vindas) 87 | ``` 88 | 89 | ## Testar alterações na imagem sem necessidade de commit 90 | 91 | No arquivo Gemfile.lock é possível alterar o repositório e o commit específico que será usado na criação da imagem, o que é necessário durante uma correção, atualização ou implementação de um novo caso, um exemplo simples pode ser visto nesse PR https://github.com/akretion/boleto_cnab_api/pull/11/files , mas também é possível alterar o Dockerfile para criar uma imagem de teste onde seja possível editar os arquivos dentro do container (o que evita subir um commit desnecessário ou com erro), para isso no arquivo Dockerfile são feitas as seguintes alterações: 92 | 93 | Instalar algum editor de texto, por exemplo VIM ou Nano (por padrão o VI já está instalado mas caracteres UTF-8 não são mostrados corretamente) e alterar o usuário **app** para o **root** para poder editar os arquivos 94 | ```bash 95 | git \ 96 | ruby-dev \ 97 | + vim \ 98 | + nano \ 99 | && rm -rf /var/cache/apk/* \ 100 | ; 101 | 102 | -USER app 103 | +USER root 104 | ``` 105 | 106 | Criação da imagem 107 | ```bash 108 | $ docker build -t akretion/boleto_cnab_api-teste . 109 | ``` 110 | 111 | Depois de iniciar a imagem podemos entrar dentro do container 112 | ```bash 113 | Localizar o container ID 114 | 115 | $ docker ps 116 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 117 | 1ea95da3a3c3 akretion/boleto_cnab_api-teste "/bin/sh -c 'bundle …" 4 minutes ago Up 4 minutes 0.0.0.0:9292->9292/tcp, :::9292->9292/tcp eloquent_noether 118 | ``` 119 | 120 | Acessando o container (No Debian usa /bin/bash no Alpine /bin/sh) 121 | ```bash 122 | $ docker exec -it /bin/sh 123 | 124 | O valor varia, nesse exemplo o comando seria 125 | 126 | $ docker exec -it 1ea95da3a3c3 /bin/sh 127 | ``` 128 | 129 | Dentro do container é preciso localizar a pasta onde está instalada a biblioteca, no exemplo é usado o comando **find** e a partir disso é possível realizar alterações necessárias 130 | ```bash 131 | /usr/src/app # find /usr -name unicred.rb 132 | /usr/lib/ruby/gems/3.3.0/bundler/gems/brcobranca-cd928e87554b/lib/brcobranca/retorno/cnab400/unicred.rb 133 | /usr/lib/ruby/gems/3.3.0/bundler/gems/brcobranca-cd928e87554b/lib/brcobranca/remessa/cnab240/unicred.rb 134 | /usr/lib/ruby/gems/3.3.0/bundler/gems/brcobranca-cd928e87554b/lib/brcobranca/remessa/cnab400/unicred.rb 135 | ``` 136 | 137 | A partir disso é possível realizar alterações necessárias, por exemplo verificar o valor de alguma variável "imprimindo" no LOG com o comando "puts" (algumas referencias https://www.dotnetperls.com/console-ruby https://www.rubyguides.com/2018/10/puts-vs-print/ http://ruby-for-beginners.rubymonstas.org/writing_methods/printing.html ) 138 | ```bash 139 | /usr/src/app # vim /usr/lib/ruby/gems/3.3.0/bundler/gems/brcobranca-cd928e87554b/lib/brcobranca/ 140 | boleto/unicred.rb 141 | 142 | def codigo_barras_segunda_parte 143 | puts "TESTE puts algum valor qualquer " + "#{agencia}" 144 | "#{agencia}#{conta_corrente}#{conta_corrente_dv}#{nosso_numero}#{nosso_numero_dv}" 145 | end 146 | end 147 | ``` 148 | 149 | Nesse exemplo ao criar um Boleto do UNICRED é possível ver no LOG o resultado do "puts" 150 | ```bash 151 | $ docker logs -f 28f2881e4dd7 152 | Puma starting in single mode... 153 | * Puma version: 6.4.2 (ruby 3.3.3-p89) ("The Eagle of Durango") 154 | * Min threads: 0 155 | * Max threads: 5 156 | * Environment: development 157 | * PID: 1 158 | * Listening on http://0.0.0.0:9292 159 | Use Ctrl-C to stop 160 | TESTE puts algum valor qualquer 1234 161 | ``` 162 | 163 | Se a imagem estiver sendo iniciada dentro de um **Docker Compose**, por exemplo por um projeto Odoo é possível ver o LOG usando: 164 | ```bash 165 | $ docker logs -f 28f2881e4dd7 166 | Puma starting in single mode... 167 | * Puma version: 6.4.2 (ruby 3.3.3-p89) ("The Eagle of Durango") 168 | * Min threads: 0 169 | * Max threads: 5 170 | * Environment: development 171 | * PID: 1 172 | * Listening on http://0.0.0.0:9292 173 | Use Ctrl-C to stop 174 | - Gracefully stopping, waiting for requests to finish 175 | === puma shutdown: 2024-07-05 19:50:05 +0000 === 176 | - Goodbye! 177 | ``` 178 | 179 | **IMPORTANTE:** por algum motivo as alterações dentro do container só tem efeito na primeira vez que o arquivo é Salvo, uma segunda alteração não tem efeito, isso pode ser algo referente ao comportamento da imagem, ou do Docker ou do Docker Compose, já que nos testes realizados esse container é iniciado e usado por outro container rodando o Odoo, é preciso investigar melhor para entender se isso é algo normal e já esperado ou se teria uma forma de corrigir, porque devido a isso para testar dessa forma está sendo necessário alterar uma vez e se for preciso fazer outra alteração sair do container fazer um kill e inicia-lo novamente. 180 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | $: << File.join(File.dirname(__FILE__), "/lib") 2 | require 'boleto_api' 3 | run BoletoApi::Server 4 | -------------------------------------------------------------------------------- /lib/boleto_api.rb: -------------------------------------------------------------------------------- 1 | require 'brcobranca' 2 | require 'grape' 3 | 4 | 5 | module BoletoApi 6 | 7 | def self.get_boleto(bank, values) 8 | clazz = Object.const_get("Brcobranca::Boleto::#{bank.camelize}") 9 | date_fields = %w[data_documento data_vencimento data_processamento] 10 | date_fields.each do |date_field| 11 | values[date_field] = Date.parse(values[date_field]) if values[date_field] 12 | end 13 | clazz.new(values) 14 | end 15 | 16 | def self.get_pagamento(values) 17 | date_fields = %w[data_vencimento data_emissao data_desconto data_segundo_desconto data_multa] 18 | date_fields.each do |date_field| 19 | values[date_field] = Date.parse(values[date_field]) if values[date_field] 20 | end 21 | values['data_vencimento'] ||= Date.current 22 | Brcobranca::Remessa::Pagamento.new(values) 23 | end 24 | 25 | class Server < Grape::API 26 | version 'v1', using: :header, vendor: 'Akretion' 27 | format :json 28 | prefix :api 29 | 30 | resource :boleto do 31 | 32 | desc 'Validate boleto data' 33 | # example of invalid attributes: 34 | # http://localhost:9292/api/boleto/validate?bank=itau&data=%7B%22valor%22:0.0,%22documento_cedente%22:%2212345678912%22,%22sacado%22:%22Claudio%20Pozzebom%22,%22sacado_documento%22:%2212345678900%22,%22conta_corrente%22:%2253678%22,%22convenio%22:12387,%22numero_documento%22:%2212345678%22%7D 35 | # boleto fields are listed here: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/boleto/base.rb 36 | params do 37 | requires :bank, type: String, desc: 'Bank' 38 | requires :data, type: String, desc: 'Boleto data as a stringified json' 39 | end 40 | get :validate do 41 | values = JSON.parse(params[:data]) 42 | boleto = BoletoApi.get_boleto(params[:bank], values) 43 | if boleto.valid? 44 | true 45 | else 46 | error!(boleto.errors.messages, 400) 47 | end 48 | end 49 | 50 | desc 'Generates boleto nosso_numero' 51 | # TODO do we also need an API endpoint for nosso_numero_dv? 52 | # example with Itau boleto with data from https://github.com/kivanio/brcobranca/blob/master/spec/brcobranca/boleto/itau_spec.rb: 53 | # http://localhost:9292/api/boleto/nosso_numero?bank=itau&data=%7B%22valor%22:0.0,%22cedente%22:%22Kivanio%20Barbosa%22,%22documento_cedente%22:%2212345678912%22,%22sacado%22:%22Claudio%20Pozzebom%22,%22sacado_documento%22:%2212345678900%22,%22agencia%22:%220810%22,%22conta_corrente%22:%2253678%22,%22convenio%22:12387,%22numero_documento%22:%2212345678%22%7D 54 | # boleto fields are listed here: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/boleto/base.rb 55 | params do 56 | requires :bank, type: String, desc: 'Bank' 57 | requires :data, type: String, desc: 'Boleto data as a stringified json' 58 | end 59 | get :nosso_numero do 60 | values = JSON.parse(params[:data]) 61 | boleto = BoletoApi.get_boleto(params[:bank], values) 62 | if boleto.valid? 63 | boleto.nosso_numero_boleto 64 | else 65 | error!(boleto.errors.messages, 400) 66 | end 67 | end 68 | 69 | desc 'Return a bolato image or pdf' 70 | # example of valid Itau boleto with data from https://github.com/kivanio/brcobranca/blob/master/spec/brcobranca/boleto/itau_spec.rb 71 | # http://localhost:9292/api/boleto?type=pdf&bank=itau&data=%7B%22valor%22:0.0,%22cedente%22:%22Kivanio%20Barbosa%22,%22documento_cedente%22:%2212345678912%22,%22sacado%22:%22Claudio%20Pozzebom%22,%22sacado_documento%22:%2212345678900%22,%22agencia%22:%220810%22,%22conta_corrente%22:%2253678%22,%22convenio%22:12387,%22numero_documento%22:%2212345678%22%7D 72 | # boleto fields are listed here: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/boleto/base.rb 73 | params do 74 | requires :bank, type: String, desc: 'Bank' 75 | requires :type, type: String, desc: 'Type: pdf|jpg|png|tif' 76 | requires :data, type: String, desc: 'Boleto data as a stringified json' 77 | end 78 | get do 79 | values = JSON.parse(params[:data]) 80 | boleto = BoletoApi.get_boleto(params[:bank], values) 81 | if boleto.valid? 82 | content_type "application/#{params[:type]}" 83 | header['Content-Disposition'] = "attachment; filename=boleto-#{params[:bank]}.#{params[:type]}" 84 | env['api.format'] = :binary 85 | boleto.send("to_#{params[:type]}".to_sym) 86 | else 87 | error!(boleto.errors.messages, 400) 88 | end 89 | end 90 | 91 | desc 'Return the image or pdf of a collection of boletos' 92 | # example of valid Itau boleto with data from https://github.com/kivanio/brcobranca/blob/master/spec/brcobranca/boleto/itau_spec.rb 93 | # and https://github.com/kivanio/brcobranca/blob/master/spec/brcobranca/boleto/caixa_spec.rb 94 | # echo '[{"valor":5.0,"cedente":"Kivanio Barbosa","documento_cedente":"12345678912","sacado":"Claudio Pozzebom","sacado_documento":"12345678900","agencia":"0810","conta_corrente":"53678","convenio":12387,"numero_documento":"12345678","bank":"itau"},{"valor": 10.00,"cedente": "PREFEITURA MUNICIPAL DE VILHENA","documento_cedente": "04092706000181","sacado": "João Paulo Barbosa","sacado_documento": "77777777777","agencia": "1825","conta_corrente": "0000528","convenio": "245274","numero_documento": "000000000000001","bank":"caixa"}]' > /tmp/boletos_data.json 95 | # curl -X POST -F type=pdf -F 'data=@/tmp/boletos_data.json' localhost:9292/api/boleto/multi > /tmp/boletos.pdf 96 | # boleto fields are listed here: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/boleto/base.rb 97 | params do 98 | requires :type, type: String, desc: 'Type: pdf|jpg|png|tif' 99 | requires :data, type: File, desc: 'json of the list of boletos, including the "bank" key' 100 | end 101 | post :multi do 102 | values = JSON.parse(params[:data][:tempfile].read()) 103 | boletos = [] 104 | errors = [] 105 | values.each do |boleto_values| 106 | bank = boleto_values.delete('bank').camelize 107 | boleto = BoletoApi.get_boleto(bank, boleto_values) 108 | if boleto.valid? 109 | boletos << boleto 110 | else 111 | errors << boleto.errors.messages 112 | end 113 | end 114 | if errors.empty? 115 | content_type "application/#{params[:type]}" 116 | header['Content-Disposition'] = "attachment; filename=boletos-#{params[:bank]}.#{params[:type]}" 117 | env['api.format'] = :binary 118 | Brcobranca::Boleto::Base.lote(boletos, formato: params[:type].to_sym) 119 | else 120 | error!(errors, 400) 121 | end 122 | end 123 | end 124 | 125 | resource :remessa do 126 | # example with data from https://github.com/kivanio/brcobranca/blob/master/spec/brcobranca/remessa/cnab400/itau_spec.rb 127 | # echo '{"carteira": "123","agencia": "1234","conta_corrente": "12345","digito_conta": "1","empresa_mae": "SOCIEDADE BRASILEIRA DE ZOOLOGIA LTDA","documento_cedente": "12345678910","pagamentos":[{"valor": 199.9,"data_vencimento": "Thu, 15 Jun 2017","nosso_numero": 123,"documento_sacado": "12345678901","nome_sacado": "PABLO DIEGO JOSÉ FRANCISCO DE PAULA JUAN NEPOMUCENO MARÍA DE LOS REMEDIOS CIPRIANO DE LA SANTÍSSIMA TRINIDAD RUIZ Y PICASSO","endereco_sacado": "RUA RIO GRANDE DO SUL São paulo Minas caçapa da silva junior","bairro_sacado": "São josé dos quatro apostolos magros","cep_sacado": "12345678","cidade_sacado": "Santa rita de cássia maria da silva","uf_sacado": "SP"}]}' > /tmp/remessa_data.json 128 | # curl -X POST -F type=cnab400 -F bank=itau -F 'data=@/tmp/remessa_data.json' localhost:9292/api/remessa 129 | # generic remessa fields are: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/remessa/base.rb 130 | # cnab240 have these extra fields: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/remessa/cnab240/base.rb 131 | # cnab400 have these extra fields: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/remessa/cnab400/base.rb 132 | # the 'pagamentos' items have these fields: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/remessa/pagamento.rb 133 | params do 134 | requires :bank, type: String, desc: 'Bank' 135 | requires :type, type: String, desc: 'Type: cnab400|cnab240' 136 | requires :data, type: File, desc: 'json of the list of pagamentos' 137 | end 138 | post do 139 | values = JSON.parse(params[:data][:tempfile].read()) 140 | pagamentos = [] 141 | errors = [] 142 | values['pagamentos'].each do |pagamento_values| 143 | pagamento = BoletoApi.get_pagamento(pagamento_values) 144 | if pagamento.valid? 145 | pagamentos << pagamento 146 | else 147 | errors << pagamento.errors.messages 148 | end 149 | end 150 | if errors.empty? 151 | values[:pagamentos] = pagamentos 152 | clazz = Object.const_get("Brcobranca::Remessa::#{params[:type].camelize}::#{params[:bank].camelize}") 153 | remessa = clazz.new(values) 154 | if remessa.valid? 155 | env['api.format'] = :binary 156 | remessa.gera_arquivo() 157 | else 158 | [remessa.errors.messages] + errors 159 | end 160 | else 161 | error!(errors, 400) 162 | end 163 | end 164 | end 165 | 166 | # to avoid returning Ruby objects, we will read the payments fields from https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/retorno/base.rb 167 | RETORNO_FIELDS = [:codigo_registro,:codigo_ocorrencia,:data_ocorrencia,:agencia_com_dv,:agencia_sem_dv,:cedente_com_dv,:convenio,:nosso_numero,:codigo_ocorrencia,:data_ocorrencia,:tipo_cobranca,:tipo_cobranca_anterior,:natureza_recebimento,:carteira_variacao,:desconto,:iof,:carteira,:comando,:data_liquidacao,:data_vencimento,:valor_titulo,:banco_recebedor,:agencia_recebedora_com_dv,:especie_documento,:data_ocorrencia,:data_credito,:valor_tarifa,:outras_despesas,:juros_desconto,:iof_desconto,:valor_abatimento,:desconto_concedito,:valor_recebido,:juros_mora,:outros_recebimento,:abatimento_nao_aproveitado,:valor_lancamento,:indicativo_lancamento,:indicador_valor,:valor_ajuste,:sequencial,:arquivo,:outros_recebimento,:motivo_ocorrencia,:documento_numero] 168 | resource :retorno do 169 | # example: 170 | # wget -O /tmp/CNAB400ITAU.RET https://raw.githubusercontent.com/kivanio/brcobranca/master/spec/arquivos/CNAB400ITAU.RET 171 | # curl -X POST -F type=cnab400 -F bank=itau -F 'data=@/tmp/CNAB400ITAU.RET.txt' localhost:9292/api/retorno 172 | # the returned payment items have these fields: https://github.com/kivanio/brcobranca/blob/master/lib/brcobranca/retorno/base.rb 173 | params do 174 | requires :bank, type: String, desc: 'Bank' 175 | requires :type, type: String, desc: 'Type: cnab400|cnab240' 176 | requires :data, type: File, desc: 'txt of the retorno file' 177 | end 178 | post do 179 | data = params[:data][:tempfile] 180 | clazz = Object.const_get("Brcobranca::Retorno::#{params[:type].camelize}::#{params[:bank].camelize}") 181 | pagamentos = clazz.load_lines(data) 182 | pagamentos.map! do |p| 183 | Hash[RETORNO_FIELDS.map{|sym| [sym, p.send(sym)]}] 184 | end 185 | JSON.generate(pagamentos) 186 | end 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /test_run.py: -------------------------------------------------------------------------------- 1 | import json 2 | import tempfile 3 | from pathlib import Path 4 | import os 5 | import subprocess 6 | import requests 7 | import time 8 | 9 | 10 | def test_run(): 11 | cmd = ["docker", "build", "-t", "akretion/boleto_cnab_api", "."] 12 | result = subprocess.run( 13 | cmd, check=False, capture_output=True, text=True, cwd=Path(__file__).parent 14 | ) 15 | assert result.returncode == 0, result.stderr + "\n" + result.stdout 16 | 17 | cmd = [ 18 | "docker", 19 | "run", 20 | "-d", 21 | "-p", 22 | "9292:9292", 23 | "--name=boleto_cnab_api", 24 | "akretion/boleto_cnab_api", 25 | ] 26 | result = subprocess.run(cmd, check=False, capture_output=True, text=True) 27 | assert result.returncode == 0, result.stderr + "\n" + result.stdout 28 | time.sleep(5) 29 | remessa_values = [ 30 | { 31 | "valor": 5.0, 32 | "cedente": "Kivanio Barbosa", 33 | "documento_cedente": "12345678912", 34 | "sacado": "Claudio Pozzebom", 35 | "sacado_documento": "12345678900", 36 | "agencia": "0810", 37 | "conta_corrente": "53678", 38 | "convenio": 12387, 39 | "nosso_numero": "12345678", 40 | "bank": "itau", 41 | }, 42 | { 43 | "valor": 10.00, 44 | "cedente": "PREFEITURA MUNICIPAL DE VILHENA", 45 | "documento_cedente": "04092706000181", 46 | "sacado": "João Paulo Barbosa", 47 | "sacado_documento": "77777777777", 48 | "agencia": "1825", 49 | "conta_corrente": "0000528", 50 | "convenio": "245274", 51 | "nosso_numero": "000000000000001", 52 | "bank": "caixa", 53 | }, 54 | ] 55 | content = json.dumps(remessa_values) 56 | with open(tempfile.mktemp(), "w") as f: 57 | file_name = f.name 58 | f.write(content) 59 | files = {"data": open(file_name, "rb")} 60 | result = requests.post( 61 | "http://localhost:9292/api/boleto/multi", 62 | data={ 63 | "type": "pdf", 64 | }, 65 | files=files, 66 | ) 67 | assert str(result.status_code)[0] == "2" 68 | 69 | cmd = ["docker", "rm", "-f", "boleto_cnab_api"] 70 | result = subprocess.run(cmd, check=False, capture_output=True, text=True) 71 | assert result.returncode == 0, result.stderr + "\n" + result.stdout 72 | --------------------------------------------------------------------------------