class Bankscrap::ING::Bank
Constants
- BASE_ENDPOINT
- CLIENT_ENDPOINT
- CURRENCY
- LOGIN_ENDPOINT
- POST_AUTH_ENDPOINT
- PRODUCTS_ENDPOINT
- REQUIRED_CREDENTIALS
Public Class Methods
new(credentials = {})
click to toggle source
Calls superclass method
# File lib/bankscrap/ing/bank.rb, line 23 def initialize(credentials = {}) super do @password = @password.to_s end end
Public Instance Methods
balances()
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 29 def balances log 'get_balances' balances = {} total_balance = 0 @accounts.each do |account| balances[account.description] = account.balance total_balance += account.balance end balances['TOTAL'] = total_balance balances end
build_transactions_request_params(start_date, end_date)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 83 def build_transactions_request_params(start_date, end_date) # The API allows any limit to be passed, but we better keep # being good API citizens and make a loop with a short limit { fromDate: start_date.strftime('%d/%m/%Y'), toDate: end_date.strftime('%d/%m/%Y'), limit: 25, offset: 0 } end
fetch_accounts()
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 42 def fetch_accounts log 'fetch_accounts' add_headers( 'Accept' => '*/*', 'Content-Type' => 'application/json; charset=utf-8' ) JSON.parse(get(PRODUCTS_ENDPOINT)).map do |account| build_account(account) if account['iban'] end.compact end
fetch_investments()
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 54 def fetch_investments log 'fetch_investments' add_headers( 'Accept' => '*/*', 'Content-Type' => 'application/json; charset=utf-8' ) JSON.parse(get(PRODUCTS_ENDPOINT)).map do |investment| build_investment(investment) if investment['investment'] end.compact end
fetch_transactions_for(account, start_date: Date.today - 1.month, end_date: Date.today)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 66 def fetch_transactions_for(account, start_date: Date.today - 1.month, end_date: Date.today) log "fetch_transactions for #{account.id}" params = build_transactions_request_params(start_date, end_date) transactions = [] loop do request = get("#{PRODUCTS_ENDPOINT}/#{account.id}/movements", params: params) json = JSON.parse(request) transactions += (json['elements'] || []).map do |transaction| build_transaction(transaction, account) end params[:offset] += params[:limit] break if (params[:offset] > json['total']) || json['elements'].blank? end transactions end
Private Instance Methods
build_account(data)
click to toggle source
Build an Account object from API data
# File lib/bankscrap/ing/bank.rb, line 200 def build_account(data) Account.new( bank: self, id: data['uuid'], name: data['name'], balance: Money.new(data['balance'] * 100, CURRENCY), available_balance: Money.new(data['availableBalance'] * 100, CURRENCY), description: (data['alias'] || data['name']), iban: data['iban'], bic: data['bic'] ) end
build_investment(data)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 213 def build_investment(data) Investment.new( bank: self, id: data['uuid'], name: data['name'], balance: data['balance'], currency: CURRENCY.iso_code, investment: data['investment'] ) end
build_transaction(data, account)
click to toggle source
Build a transaction object from API data
# File lib/bankscrap/ing/bank.rb, line 225 def build_transaction(data, account) amount = Money.new(data['amount'] * 100, CURRENCY) Transaction.new( account: account, id: data['uuid'], amount: amount, effective_date: Date.strptime(data['effectiveDate'], '%d/%m/%Y'), description: data['description'], balance: Money.new(data['balance'] * 100, CURRENCY) ) end
correct_positions(pinpad_numbers, positions)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 191 def correct_positions(pinpad_numbers, positions) positions.map do |position| # Positions array is 1-based number_to_find = @password[position - 1].to_i pinpad_numbers.index(number_to_find) end end
extract_images_from_pinpad(pinpad)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 142 def extract_images_from_pinpad(pinpad) pinpad.map do |string| extract_info_from_encoded_png(string) end end
extract_info_from_encoded_png(encoded_png)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 148 def extract_info_from_encoded_png(encoded_png) decoded = Base64.decode64(encoded_png) # PNG format first bytes are headers that we can discard. We know where the data chunk starts, # so we can directly read the image data. See: https://www.w3.org/TR/PNG-Structure.html length = decoded.slice(33, 4).unpack('L>').first data = decoded.slice(41, length) Zlib::Inflate.inflate(data) # We decompress the data .delete("\u0000") # Levenshtein distance doesn't work with null bytes .slice(2_000, 5_000) # The whole image is too slow to compare, we can use a chunk in the middle. end
find_more_similar_image(sorted_pinpad_images, image, candidates)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 177 def find_more_similar_image(sorted_pinpad_images, image, candidates) more_similar_number = -1 min = 99_999 # An arbitrary, high enough upper limit candidates.each do |j| l = Levenshtein.distance(image, sorted_pinpad_images[j]) # If the distance is lower, we have found a better candidate if l < min min = l more_similar_number = j end end more_similar_number end
login()
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 96 def login ticket = pass_pinpad(request_pinpad_positions) post_auth(ticket) end
pass_pinpad(positions)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 126 def pass_pinpad(positions) fields = { pinPositions: positions } response = put(LOGIN_ENDPOINT, fields: fields.to_json) JSON.parse(response)['ticket'] end
post_auth(ticket)
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 132 def post_auth(ticket) add_headers( 'Accept' => 'application/json, text/javascript, */*; q=0.01', 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' ) params = "ticket=#{ticket}&device=desktop" post(POST_AUTH_ENDPOINT, fields: params) end
recognize_pinpad_numbers(received_pinpad)
click to toggle source
For each image received, we compare it with the images of the numbers we already know, in order to find the most similar one (using Levenshtein's string comparison).
Note: We can't compare directly the strings we received, even after base64 decoding them, because PNG format compress with Zlib the data of the image.
# File lib/bankscrap/ing/bank.rb, line 164 def recognize_pinpad_numbers(received_pinpad) received_pinpad_images = extract_images_from_pinpad(received_pinpad) sorted_pinpad_images = extract_images_from_pinpad(Config::SORTED_PINPAD) received_pinpad_numbers = [] candidates = *0..9 0.upto(9) do |i| number = find_more_similar_image(sorted_pinpad_images, received_pinpad_images[i], candidates) candidates.delete(number) # We can reduce our search space received_pinpad_numbers << number end received_pinpad_numbers end
request_pinpad_positions()
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 101 def request_pinpad_positions add_headers( 'Accept' => 'application/json, text/javascript, */*; q=0.01', 'Content-Type' => 'application/json; charset=utf-8' ) params = request_pinpad_positions_params response = JSON.parse(post(LOGIN_ENDPOINT, fields: params.to_json)) pinpad_numbers = recognize_pinpad_numbers(response['pinpad']) correct_positions(pinpad_numbers, response['pinPositions']) end
request_pinpad_positions_params()
click to toggle source
# File lib/bankscrap/ing/bank.rb, line 114 def request_pinpad_positions_params { loginDocument: { documentType: 0, document: @dni }, birthday: @birthday.to_s, companyDocument: nil, device: 'desktop' } end