baa_chan 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6dd4992c1495048ce9123656452c1d9e2ed8ce3dffd2c7f070172c958f748cca
4
+ data.tar.gz: b6a55877a52d39e01aeb3bb6f11d54a6f7f9a86e5622b70ba4e82e13e1302701
5
+ SHA512:
6
+ metadata.gz: 392c751ceccd693dfb2a85d6140b69c8d1bfea4f08088917f8de8e0965deef3234b839aaba431a35ea996041cf0c4a50932b3353039e184cfda1a3b2fd178869
7
+ data.tar.gz: 62c4a801ea57f5ff746a3edec50458ae18ad4e3ca8d01330bbdbf259ebe3d4eb3dd7132b4e46e654807558c6fe2aafdf77543c7f1d768c9f3d9c7524c96dc5fb
data/bin/baa_chan ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'baa_chan'
5
+ puts BaaChan::Reader.new(ARGV[0]).call.to_builder
data/lib/baa_chan.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'baa_chan/reader'
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BaaChan
4
+ class Costs
5
+ attr_accessor :clearing_fee, :registration_fee, :emoluments,
6
+ :brokerage, :iss, :irrf, :pis_cofins
7
+
8
+ def initialize(brokerage, clearing_fee, registration_fee, emoluments, options)
9
+ @brokerage = brokerage
10
+ @clearing_fee = clearing_fee
11
+ @registration_fee = registration_fee
12
+ @emoluments = emoluments
13
+ @iss = options[:iss] || 0.0
14
+ @irrf = options[:irrf] || 0.0
15
+ @pis_cofins = options[:pis_cofins] || 0.0
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Layout
4
+ LAYOUTS_PATH = 'lib/baa_chan/layouts'
5
+
6
+ def initialize(broker)
7
+ @broker = broker
8
+ end
9
+
10
+ def attributes
11
+ @attributes ||= YAML.safe_load(File.read(File.join(LAYOUTS_PATH, "#{@broker}.yml")))
12
+ end
13
+
14
+ def line
15
+ attributes[caller_locations.first.label]['line'].to_i
16
+ end
17
+
18
+ def index(attr_name = nil)
19
+ attr = attr_name || caller_locations.first.label
20
+
21
+ attributes[attr]['index'].to_i
22
+ end
23
+
24
+ def trade_prefix
25
+ attributes['trades']['prefix']
26
+ end
27
+
28
+ def regexp_for(attr)
29
+ attributes[attr]['regexp']
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ broker:
2
+ line: 4
3
+ trade_confirmation_number:
4
+ line: 2
5
+ index: 0
6
+ trade_date:
7
+ line: 2
8
+ index: 2
9
+ trades:
10
+ prefix: 1-BOVESPA
11
+ operation:
12
+ index: 1
13
+ ticker:
14
+ index: 3
15
+ quantity:
16
+ index: 7
17
+ price:
18
+ index: 8
19
+ clearing_fee:
20
+ regexp: .*Taxa de liquidação.*
21
+ index: 7
22
+ registration_fee:
23
+ regexp: .*Taxa de Registro.*
24
+ index: 7
25
+ emoluments:
26
+ regexp: .*Emolumentos.*
27
+ index: 1
28
+ irrf:
29
+ regexp: .*I\.R\.R\.F.*
30
+ index: 5
@@ -0,0 +1,36 @@
1
+ broker:
2
+ line: 4
3
+ trade_confirmation_number:
4
+ line: 2
5
+ index: 0
6
+ trade_date:
7
+ line: 2
8
+ index: 2
9
+ trades:
10
+ prefix: 1-BOVESPA
11
+ operation:
12
+ index: 1
13
+ ticker:
14
+ index: 3
15
+ quantity:
16
+ index: 6
17
+ price:
18
+ index: 7
19
+ clearing_fee:
20
+ regexp: .*Taxa de liquidação.*
21
+ index: 7
22
+ registration_fee:
23
+ regexp: .*Taxa de Registro.*
24
+ index: 7
25
+ emoluments:
26
+ regexp: .*Emolumentos.*
27
+ index: 1
28
+ brokerage:
29
+ regexp: ^A coluna Q.*
30
+ index: 9
31
+ iss:
32
+ regexp: ^ISS \( SÃO PAULO \).*
33
+ index: 5
34
+ pis_cofins:
35
+ regexp: ^ISS/PIS/COFINS.*
36
+ index: 1
@@ -0,0 +1,36 @@
1
+ broker:
2
+ line: 3
3
+ trade_confirmation_number:
4
+ line: 2
5
+ index: 0
6
+ trade_date:
7
+ line: 2
8
+ index: 2
9
+ trades:
10
+ prefix: BM&FBOVESPA S/A.
11
+ operation:
12
+ index: 2
13
+ ticker:
14
+ index: 6
15
+ quantity:
16
+ index: 9
17
+ price:
18
+ index: 10
19
+ clearing_fee:
20
+ regexp: .*Taxa de Liquidação.*
21
+ index: 7
22
+ registration_fee:
23
+ regexp: .*Taxa de Registro.*
24
+ index: 7
25
+ emoluments:
26
+ regexp: .*Emolumentos.*
27
+ index: 5
28
+ brokerage:
29
+ regexp: ^Corretagem.*
30
+ index: 1
31
+ iss:
32
+ regexp: ^ISS \(SAO PAULO\).*
33
+ index: 3
34
+ irrf:
35
+ regexp: .*I\.R\.R\.F.*
36
+ index: 14
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'baa_chan/trade_confirmation'
4
+ require 'baa_chan/trade'
5
+ require 'baa_chan/costs'
6
+ require 'date'
7
+ require 'yaml'
8
+
9
+ module BaaChan
10
+ class CostsParserError < StandardError; end
11
+
12
+ class Parser
13
+ def initialize(lines, layout)
14
+ @lines = lines
15
+ @layout = layout
16
+ end
17
+
18
+ def call
19
+ TradeConfirmation.new(broker, trade_confirmation_number, trade_date, trades, costs)
20
+ end
21
+
22
+ private
23
+
24
+ def broker
25
+ @broker ||= @lines[@layout.line]
26
+ end
27
+
28
+ def trade_confirmation_number
29
+ @trade_confirmation_number ||= @lines[@layout.line].split[@layout.index]
30
+ end
31
+
32
+ def trade_date
33
+ @trade_date ||= Date.parse @lines[@layout.line].split[@layout.index]
34
+ end
35
+
36
+ def trades
37
+ @trades ||= TradeParser.new(@lines, @layout).parse
38
+ end
39
+
40
+ def costs
41
+ @costs ||= CostsParser.new(@lines, @layout).parse
42
+ end
43
+ end
44
+
45
+ class TradeParser
46
+ def initialize(lines, layout)
47
+ @lines = lines
48
+ @layout = layout
49
+ end
50
+
51
+ def parse
52
+ @lines.each_with_object([]) do |line, trades|
53
+ next unless line.include? @layout.trade_prefix
54
+
55
+ @trade_line = line
56
+ trades << Trade.new(operation, ticker, quantity, price)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def operation
63
+ @trade_line.split[@layout.index] == 'V' ? 'Sell' : 'Buy'
64
+ end
65
+
66
+ def ticker
67
+ @trade_line.split[@layout.index]
68
+ end
69
+
70
+ def quantity
71
+ @trade_line.split[@layout.index].to_i
72
+ end
73
+
74
+ def price
75
+ @trade_line.split[@layout.index].gsub(',', '.').to_f
76
+ end
77
+ end
78
+
79
+ class CostsParser
80
+ def initialize(lines, layout)
81
+ @lines = lines
82
+ @layout = layout
83
+ end
84
+
85
+ def parse
86
+ Costs.new(brokerage, clearing_fee, registration_fee, emoluments, { iss: iss, irrf: irrf, pis_cofins: pis_cofins })
87
+ rescue StandardError => e
88
+ raise CostsParserError, e.message
89
+ end
90
+
91
+ private
92
+
93
+ def value_for(attr_name)
94
+ @lines.find { |line| line.match? @layout.regexp_for(attr_name) }
95
+ .split[@layout.index(attr_name)]
96
+ .gsub(',', '.')
97
+ .to_f
98
+ end
99
+
100
+ def brokerage
101
+ value_for('brokerage')
102
+ rescue NoMethodError
103
+ 0.0
104
+ end
105
+
106
+ def clearing_fee
107
+ value_for('clearing_fee')
108
+ end
109
+
110
+ def registration_fee
111
+ value_for('registration_fee')
112
+ end
113
+
114
+ def emoluments
115
+ value_for('emoluments')
116
+ end
117
+
118
+ def iss
119
+ value_for('iss')
120
+ rescue NoMethodError
121
+ 0.0
122
+ end
123
+
124
+ def irrf
125
+ value_for('irrf')
126
+ rescue NoMethodError
127
+ 0.0
128
+ end
129
+
130
+ def pis_cofins
131
+ value_for('pis_cofins')
132
+ rescue NoMethodError
133
+ 0.0
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ module BaaChan
6
+ class UnreadablePDFError < StandardError; end
7
+
8
+ class InvalidExtensionError < StandardError; end
9
+
10
+ class PdfToText
11
+ def self.call(source)
12
+ raise InvalidExtensionError unless source.match /\.pdf$/
13
+
14
+ content = `pdftotext -layout #{source} -`
15
+
16
+ raise UnreadablePDFError unless $CHILD_STATUS.success?
17
+
18
+ content
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BaaChan
4
+ class MalformedPDFError < StandardError; end
5
+
6
+ class UnknownLayoutError < StandardError; end
7
+
8
+ class Reader
9
+ BROKER_LIST = {
10
+ singulare: 'Singulare - corretora de titulos de valores mobiliarios',
11
+ genial: 'GENIAL INVESTIMENTOS CORRETORA DE VALORES MOBILIÁRIOS S.A.',
12
+ clear: 'CLEAR CORRETORA - GRUPO XP'
13
+ }.freeze
14
+
15
+ def initialize(source)
16
+ @source = source
17
+ end
18
+
19
+ def call
20
+ parse(PdfToText.call(@source))
21
+ end
22
+
23
+ private
24
+
25
+ def parse(text)
26
+ lines = sanitize(text)
27
+
28
+ layout = Layout.new(detect_layout(lines))
29
+
30
+ Parser.new(lines, layout).call
31
+ end
32
+
33
+ def sanitize(content)
34
+ content.split("\n").map(&:strip)
35
+ end
36
+
37
+ def detect_layout(lines)
38
+ broker = if lines[3] == BROKER_LIST[:singulare]
39
+ 'singulare'
40
+ elsif lines[4] == BROKER_LIST[:genial]
41
+ 'genial'
42
+ elsif lines[4] == BROKER_LIST[:clear]
43
+ 'clear'
44
+ else
45
+ raise UnknownLayoutError
46
+ end
47
+
48
+ return broker
49
+ end
50
+ end
51
+ end
52
+
53
+ require 'baa_chan/pdf_to_text'
54
+ require 'baa_chan/parser'
55
+ require 'baa_chan/layout'
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BaaChan
4
+ class Trade
5
+ attr_accessor :operation, :ticker, :quantity, :price
6
+
7
+ def initialize(operation, ticker, quantity, price)
8
+ @operation = operation
9
+ @ticker = ticker
10
+ @quantity = quantity
11
+ @price = price
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jbuilder'
4
+
5
+ module BaaChan
6
+ class TradeConfirmation
7
+ attr_accessor :broker, :trade_confirmation_number, :trade_date, :trades, :costs
8
+
9
+ def initialize(broker, trade_confirmation_number, trade_date, trades, costs)
10
+ @broker = broker
11
+ @trade_confirmation_number = trade_confirmation_number
12
+ @trade_date = trade_date
13
+ @trades = trades
14
+ @costs = costs
15
+ end
16
+
17
+ def total_cost
18
+ @costs.brokerage +
19
+ @costs.registration_fee +
20
+ @costs.emoluments +
21
+ @costs.pis_cofins +
22
+ @costs.clearing_fee
23
+ end
24
+
25
+ def to_builder
26
+ Jbuilder.encode do |json|
27
+ json.call(
28
+ self,
29
+ :broker,
30
+ :trade_confirmation_number,
31
+ :trade_date
32
+ )
33
+
34
+ json.trades @trades do |trade|
35
+ json.operation trade.operation
36
+ json.ticker trade.ticker
37
+ json.quantity trade.quantity
38
+ json.price trade.price
39
+ end
40
+
41
+ json.costs do
42
+ json.brokerage @costs.brokerage
43
+ json.clearing_fee @costs.clearing_fee
44
+ json.registration_fee @costs.registration_fee
45
+ json.emoluments @costs.emoluments
46
+ json.iss @costs.iss if @costs.iss
47
+ json.irrf @costs.irrf if @costs.irrf
48
+ json.pis_cofins @costs.pis_cofins if @costs.pis_cofins
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: baa_chan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Diogo Noda
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry-byebug
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.9.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.9.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.10.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.10.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: jbuilder
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.11.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.11.2
55
+ description: |
56
+ BaaChan will read your trade confirmations and provide you
57
+ details about your trades helping you out handling your
58
+ finances
59
+ email: diogotnoda@gmail.com
60
+ executables:
61
+ - baa_chan
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - bin/baa_chan
66
+ - lib/baa_chan.rb
67
+ - lib/baa_chan/costs.rb
68
+ - lib/baa_chan/layout.rb
69
+ - lib/baa_chan/layouts/clear.yml
70
+ - lib/baa_chan/layouts/genial.yml
71
+ - lib/baa_chan/layouts/singulare.yml
72
+ - lib/baa_chan/parser.rb
73
+ - lib/baa_chan/pdf_to_text.rb
74
+ - lib/baa_chan/reader.rb
75
+ - lib/baa_chan/trade.rb
76
+ - lib/baa_chan/trade_confirmation.rb
77
+ homepage: https://rubygems.org/gems/baa_chan
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 3.0.0
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.2.3
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Trade Confirmation Reader
100
+ test_files: []