diff --git a/Gemfile b/Gemfile index b507caf..8790838 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,11 @@ gem 'activesupport' gem 'base32' gem 'colorize' +# Dumper: SaltEdge +gem 'rest-client' +gem 'json' +gem 'pry' + group :development do gem 'rubocop', '~> 0.52.1', require: false gem 'rubocop-rspec' diff --git a/README.md b/README.md index eed5f16..aae2d7a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This is a ruby script that **pulls your transactions from your banks** and impor * Most German and Austrian banks _(all banks that implement the FinTS standard)_ * BBVA Spain _(private accounts only)_ * N26 +* PSD2 Banks via SaltEdge API (tested with German bank: DKB) **💡 Check out the [configuration guides for the dumpers and banks](https://github.com/schurig/ynab-bank-importer/wiki#supported-dumpers)**. @@ -37,8 +38,6 @@ The script also includes some additional logic like detecting internal transacti # Known Problems -* [Most banks can't be used anymore because of the PSD2](https://github.com/schurig/ynab-bank-importer/issues/74) -> We're investigating in alternative approaches to gain access to the latest transactions.. * Please read the notes in each Dumper _[(see Wiki)](https://github.com/schurig/ynab-bank-importer/wiki#supported-dumpers)_ to understand the limitations ____________________ diff --git a/lib/dumper.rb b/lib/dumper.rb index 9083022..dd157a0 100644 --- a/lib/dumper.rb +++ b/lib/dumper.rb @@ -7,6 +7,8 @@ def self.get_dumper(name) Dumper::Bbva when :n26 Dumper::N26 + when :saltedge + Dumper::SaltEdge when :fints Dumper::Fints else @@ -16,6 +18,7 @@ def self.get_dumper(name) # rubocop:disable Metrics/MethodLength def to_ynab_transaction(transaction) + #return nil if date(transaction) < Date.parse('2019-09-05') return nil if date(transaction).nil? || date(transaction) > Date.today ::TransactionCreator.call( account_id: account_id, diff --git a/lib/dumper/saltedge.rb b/lib/dumper/saltedge.rb new file mode 100644 index 0000000..63efc4a --- /dev/null +++ b/lib/dumper/saltedge.rb @@ -0,0 +1,131 @@ +class Dumper + class SaltEdge < Dumper + require 'digest/md5' + require 'rest-client' + require 'json' + + WITHDRAWAL_CATEGORIES = [ + 'micro-v2-atm', + 'micro-v2-cash26' + ].freeze + + + def initialize(params = {}) + + @merchant_lookup = {} + @ynab_id = params.fetch('ynab_id') + @saltedge_app_id = params.fetch('saltedge_app_id') + @saltedge_secret = params.fetch('saltedge_secret') + @saltedge_connection_id = params.fetch('saltedge_connection_id') + @saltedge_account_id = params.fetch('saltedge_account_id') + @saltedge_transaction_count = params.fetch('saltedge_transaction_count') + @iban = params.fetch('iban') + end + + def request(method, url, params={}, payload={}) + + RestClient::Request.execute( + method: method, + url: url, + payload: payload, + log: Logger.new(STDOUT), + headers: { + "Accept" => "application/json", + "Content-type" => "application/json", + :params => params, + "App-Id" => @saltedge_app_id, + "Secret" => @saltedge_secret + } + ) + rescue RestClient::Exception => error + pp JSON.parse(error.response) + end + + def fetch_transactions + + data = request(:get, "https://www.saltedge.com/api/v5/transactions", + { + :connection_id => @saltedge_connection_id, + :account_id => @saltedge_account_id, + :per_page => @saltedge_transaction_count + }) + + transactions = JSON.parse(data.body)['data'] + + merchants = Set[] + + transactions.each do |t| + merchants.add(t['extra']['merchant_id']) + end + + merchants_details = request(:post, "https://www.saltedge.com/api/v4/merchants", + { + :connection_id => @saltedge_connection_id, + :account_id => @saltedge_account_id, + :per_page => @saltedge_transaction_count + }, '{"data": ' + merchants.to_json + '}') + + + merchants_result = JSON.parse(merchants_details.body)['data'] + + merchants_result.each do |t| + @merchant_lookup[t['id']] = t['names'][0]['value'] + end + + transactions.select { |t| accept?(t) } + .map { |t| to_ynab_transaction(t) } + end + + def accept?(transaction) + return transaction['status'] == 'posted' + end + + private + + def account_id + @ynab_id + end + + def date(transaction) + Date.parse(transaction['made_on']) + end + + def payee_name(transaction) + merchant_id = transaction['extra']['merchant_id'] + @merchant_lookup[merchant_id].try(:strip) + + end + + def payee_iban(transaction) + return nil + end + + def category_name(transaction) + return nil + end + + def memo(transaction) + [ + transaction['description'], + ].join(' ').try(:strip) + end + + def amount(transaction) + (transaction['amount'].to_f * 1000).to_i + end + + def withdrawal?(transaction) + WITHDRAWAL_CATEGORIES.include?(transaction['category']) + end + + def import_id(transaction) + data = [transaction['id'], + transaction['account_id'], + transaction['amount'], + transaction['description']].join + + Digest::MD5.hexdigest(data) + end + + end +end