diff --git a/ipizza.gemspec b/ipizza.gemspec index 623dc58..ed133c2 100644 --- a/ipizza.gemspec +++ b/ipizza.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |s| s.homepage = 'https://github.com/Voog/ipizza' s.summary = 'Implements iPizza protocol to communicate with Estonian Banks' s.description = 'Simplifies generating payment requests and parsing responses from banks when using iPizza protocol.' - + s.add_development_dependency 'rspec', '~> 2.9.0' s.add_development_dependency 'guard-rspec' s.add_development_dependency 'rb-fsevent' diff --git a/lib/ipizza.rb b/lib/ipizza.rb index 1614dce..8c05077 100644 --- a/lib/ipizza.rb +++ b/lib/ipizza.rb @@ -16,3 +16,4 @@ require 'ipizza/provider/sampo' require 'ipizza/provider/krediidipank' require 'ipizza/provider/nordea' +require 'ipizza/provider/luminor' diff --git a/lib/ipizza/config.rb b/lib/ipizza/config.rb index d16134a..7733f9a 100644 --- a/lib/ipizza/config.rb +++ b/lib/ipizza/config.rb @@ -8,10 +8,10 @@ class << self def load_from_file(yaml_path) @certs_root = File.dirname(yaml_path) - + load_from_hash(YAML::load_file(yaml_path)) end - + def load_from_hash(config) config.each do |bank, params| params.each do |param, value| @@ -21,19 +21,19 @@ def load_from_hash(config) end end end - + def configure yield self end - + def method_missing(m, *args) - if /^(lhv|swedbank|seb|sampo|krediidipank|nordea)_(.*)=$/ =~ m.to_s + if /^(lhv|swedbank|seb|sampo|krediidipank|nordea|luminor)_(.*)=$/ =~ m.to_s clz = Ipizza::Provider.const_get($1.capitalize) key = $2 value = args.first - + value = load_certificate(value) if /^file_(cert|key)/ =~ key - + if clz.respond_to?(:"#{key}=") return clz.send(:"#{key}=", *[value]) end @@ -41,23 +41,23 @@ def method_missing(m, *args) super end - + private - + def load_certificate(file_path) if File.exist?(file_path) file_path else file_path = File.expand_path(File.join(certs_root, file_path)) end - + if File.exist?(file_path) file_path else raise "Could not load certificate from file '#{file_path}'" end end - + end end end diff --git a/lib/ipizza/provider.rb b/lib/ipizza/provider.rb index 42ae081..8ac5176 100644 --- a/lib/ipizza/provider.rb +++ b/lib/ipizza/provider.rb @@ -14,8 +14,8 @@ def get(provider_name) Ipizza::Provider::Sampo.new when 'krep', 'krediidipank' Ipizza::Provider::Krediidipank.new - when 'nordea' - Ipizza::Provider::Nordea.new + when 'luminor', 'testluminor' + Ipizza::Provider::Luminor.new end end end diff --git a/lib/ipizza/provider/luminor.rb b/lib/ipizza/provider/luminor.rb new file mode 100644 index 0000000..58fc76d --- /dev/null +++ b/lib/ipizza/provider/luminor.rb @@ -0,0 +1,4 @@ +module Ipizza::Provider + class Luminor < Ipizza::Provider::Base + end +end diff --git a/lib/ipizza/provider/nordea.rb b/lib/ipizza/provider/nordea.rb index f8bde17..aacfd58 100644 --- a/lib/ipizza/provider/nordea.rb +++ b/lib/ipizza/provider/nordea.rb @@ -1,77 +1,4 @@ module Ipizza::Provider - - # TODO: configure whether use sha-1 or md5 for signing and verification - class Nordea - - require 'ipizza/provider/nordea/payment_request' - require 'ipizza/provider/nordea/payment_response' - require 'ipizza/provider/nordea/authentication_request' - require 'ipizza/provider/nordea/authentication_response' - - class << self - attr_accessor :payments_service_url, :payments_return_url, :payments_reject_url, :payments_cancel_url - attr_accessor :payments_rcv_id, :payments_language - attr_accessor :auth_service_url, :auth_return_url, :auth_reject_url, :auth_cancel_url, :auth_language - attr_accessor :auth_rcv_id - attr_accessor :file_key, :rcv_account, :rcv_name, :confirm, :keyvers - end - - def payment_request(payment, service = 1002) - req = Ipizza::Provider::Nordea::PaymentRequest.new - req.service_url = self.class.payments_service_url - req.params = { - 'VERSION' => '0003', - 'STAMP' => payment.stamp, - 'RCV_ID' => self.class.payments_rcv_id, - # 'RCV_ACCOUNT' => self.rcv_account, - # 'RCV_NAME' => self.rcv_name, - 'LANGUAGE' => self.class.payments_language, - 'AMOUNT' => sprintf('%.2f', payment.amount), - 'REF' => Ipizza::Util.sign_731(payment.refnum), - 'DATE' => 'EXPRESS', - 'MSG' => payment.message, - 'CONFIRM' => self.class.confirm, - 'CUR' => payment.currency, - 'KEYVERS' => self.class.keyvers, - 'REJECT' => self.class.payments_reject_url, - 'RETURN' => self.class.payments_return_url, - 'CANCEL' => self.class.payments_cancel_url - } - - req.sign(self.class.file_key) - req - end - - def payment_response(params) - response = Ipizza::Provider::Nordea::PaymentResponse.new(params) - response.verify(self.class.file_key) - return response - end - - def authentication_request - req = Ipizza::Provider::Nordea::AuthenticationRequest.new - req.service_url = self.class.auth_service_url - req.params = { - 'ACTION_ID' => '701', - 'VERS' => '0002', - 'RCVID' => self.class.auth_rcv_id, - 'LANGCODE' => self.class.auth_language, - 'STAMP' => Time.now.strftime('%Y%m%d%H%M%S'), - 'IDTYPE' => '02', - 'KEYVERS' => self.class.keyvers, - 'RETLINK' => self.class.auth_return_url, - 'CANLINK' => self.class.auth_cancel_url, - 'REJLINK' => self.class.auth_reject_url, - 'ALG' => '01' - } - req.sign(self.class.file_key) - req - end - - def authentication_response(params) - response = Ipizza::Provider::Nordea::AuthenticationResponse.new(params) - response.verify(self.class.file_key) - return response - end + class Nordea < Ipizza::Provider::Base end end diff --git a/lib/ipizza/version.rb b/lib/ipizza/version.rb index 2525cb1..9fbd7c3 100644 --- a/lib/ipizza/version.rb +++ b/lib/ipizza/version.rb @@ -1,3 +1,3 @@ module Ipizza - VERSION = '2.0.1' + VERSION = '2.1.0' end diff --git a/spec/certificates/pangalink_luminor_bank.cert.pem b/spec/certificates/pangalink_luminor_bank.cert.pem new file mode 100644 index 0000000..cec0ebb --- /dev/null +++ b/spec/certificates/pangalink_luminor_bank.cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+jCCAeICCQDuCELf8TcV7DANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJF +RTEOMAwGA1UECAwFVGFydHUxDjAMBgNVBAcMBVRhcnR1MRAwDgYDVQQKDAdNaWxl +ZWRpMB4XDTIwMTIwMzA5NDkxMloXDTI1MTIwMTA5NDkxMlowPzELMAkGA1UEBhMC +RUUxDjAMBgNVBAgMBVRhcnR1MQ4wDAYDVQQHDAVUYXJ0dTEQMA4GA1UECgwHTWls +ZWVkaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALmozE2DIZyCddrW +AVoVkNWYRYDytAi5lefa5+PEAQLGFzzfTBRnClla/Qerfe4NBGjyiwu8W6VeLGQ9 ++GGDw5uEQumII2t0f1vpKWiauyfM+MlSOB6cobf9bwlgs9gBss/PEAa8E4MhpS60 +p7KNfR1qcq3AWgxpjE9ns7KheARW1q0vNZFzPkONyleZ4HJIC8rlta38/VMvbPnp +PLbMnVOvrOsxeDMCgGpD9hupxVsFnhsv/Y4F14W7f4XHl36FhDjP8dkrgrkQE4E1 +dRnxx6GC4Lv71rQDu+gmgyKtZWmsym1b1Tzw3aCF27J1EReHvCxtkDfMH1VzQSad +FGF4yXsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQfs7p7MLZqtm9QxBkGcYALDw +q/f+5b+n1uPdFcXu8K26hG6RiVZTXUJrIqR71kGLG0mBJ3V6+vAM9gjsUJs20uiZ +qvdV2xQglTpJlNTG4Je3Sq4Lbyz4w+8e9VVrE3atfARHxA5FIewCviJNdXS+xeuh +GqKaGqLSf4sJlbe3hE69hzlDXmzUE+/OhoaSPS3zFAzcp9m50o7InhYy80eKDRFS +hBIDbLnyzb+97If9BMOhqQ/nfCrsX6TiTiqhIFB+qNSIZt733fx9AD3AwSxM51RC +V232swdtXx9d39S/DskhMkXkOiqpr+0P87Qyy8Y0/FoWO57i/jXb0uCccPatdw== +-----END CERTIFICATE----- diff --git a/spec/certificates/pangalink_luminor_bank_cert.pem b/spec/certificates/pangalink_luminor_bank_cert.pem new file mode 100644 index 0000000..8870a6e --- /dev/null +++ b/spec/certificates/pangalink_luminor_bank_cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICnzCCAYegAwIBAgIBATANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhCYW5r +bGluazAeFw0yMDEyMDMxMDUwNDJaFw0yMjEyMDMxMDUwNDJaMBMxETAPBgNVBAMM +CEJhbmtsaW5rMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgI7m13+Z +2B3aBtXE4/2YmsG9koPpD7RHj5B7uEozBCNM1MhfYoE7R7vP1tPm7lD/oJxKk0G3 +gpODtGDqo+kMfI+TlJ/w2KzwS5YPEop7aliEQ1bbwjjjY2tXYwsBYa0uRvkbwizH +lp3PHmr4DBl+JxyETIWGX623nZxZcHAvwcM6VpTtd9+KEpgToQ2gPHHj4/svQfwm +mzhdEwFZjwjCaOEHssaUcLsyvKVoJ5OJqBWS1e0mVqWsuJJJnkeG7NVXY2LWDDfP +iGogVz9hqqxfNKhCTLGMCG8k8VDQdLTaR+vvmXSUBLh+4TbFA7ZPLSvgbRu3995F +CCY/wADRophRVQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAcTtcH4m//FyGrNC2h +rcry6wt843xclEkLQSSNGz9wVCQW/D8I0VNjFPk4PgoUh4bTr+mKrZ6UrrIwBuO6 +oFj2Nh+Y1wgkpd3LMKae5DvWLE4taZ4ylBGSUXOpQ062+pOtUldZQxBiczaUJWm4 +4Ns+27Zi5oGyTNnlZXfguEnt54KA+63S4aiHWUSaaXkRN62LeNuyrX0ZdITEfTaz +0ZvVcn6FbL6OnsqoZn2MF9+xZfMqlu/K5HfGl928v0fzY8/jQygxUoGFIBfxhn46 +NniPM9xtnPSu11ut1n0PQPP8K94+MUJz7GDw9o9z3X0zgy8SOFSpY8HNp0MEjN7l +MT8n +-----END CERTIFICATE----- diff --git a/spec/config/config.yml b/spec/config/config.yml index e6a3490..b100140 100644 --- a/spec/config/config.yml +++ b/spec/config/config.yml @@ -42,6 +42,17 @@ seb: encoding: UTF-8 snd_id: sender +luminor: + service_url: https://banklink.luminor.ee/test + return_url: http://test.local/seb + cancel_url: http://test.local/seb + login: dealer + file_cert: ../certificates/pangalink_seb_bank_cert.pem + file_key: ../certificates/pangalink_seb_user_key.pem + key_secret: foobar + encoding: UTF-8 + snd_id: sender + nordea: payments_service_url: https://netbank.nordea.com/pnbepaytest/epayn.jsp payments_return_url: http://test.local/nordea diff --git a/spec/ipizza/provider/luminor_spec.rb b/spec/ipizza/provider/luminor_spec.rb new file mode 100644 index 0000000..7a2c763 --- /dev/null +++ b/spec/ipizza/provider/luminor_spec.rb @@ -0,0 +1,89 @@ +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') + +describe Ipizza::Provider::Luminor do + let(:response_time) { Ipizza::Util.time_to_iso8601(Time.now) } + let(:bank_key) { File.expand_path('../../../certificates/pangalink_luminor_bank_key.pem', __FILE__) } + + describe '#payment_request' do + let(:payment) { Ipizza::Payment.new(stamp: 1, amount: '123.34', refnum: 1, message: 'Payment message', currency: 'EUR') } + + before(:each) do + req_time = Time.now + Time.stub!(:now).and_return(req_time) + end + + it 'should sign the request' do + req = Ipizza::Provider::Luminor.new.payment_request(payment) + params = { + 'VK_SERVICE' => '1012', + 'VK_VERSION' => '008', + 'VK_SND_ID' => Ipizza::Provider::Luminor.snd_id, + 'VK_STAMP' => payment.stamp, + 'VK_AMOUNT' => sprintf('%.2f', payment.amount), + 'VK_CURR' => payment.currency, + 'VK_REF' => Ipizza::Util.sign_731(payment.refnum), + 'VK_MSG' => payment.message, + 'VK_RETURN' => Ipizza::Provider::Luminor.return_url, + 'VK_CANCEL' => Ipizza::Provider::Luminor.cancel_url, + 'VK_DATETIME' => Ipizza::Util.time_to_iso8601(Time.now) + } + signature = Ipizza::Util.sign(Ipizza::Provider::Luminor.file_key, Ipizza::Provider::Luminor.key_secret, Ipizza::Util.mac_data_string(params, Ipizza::Request::PARAM_ORDER['1012'])) + req.sign_params['VK_MAC'].should == signature + end + end + + describe '#payment_response' do + let(:params) { + { + 'VK_SERVICE' => '1111', 'VK_VERSION' => '008', 'VK_SND_ID' => 'LUMINOR', 'VK_REC_ID' => 'sender', + 'VK_STAMP' => '20150111000004', 'VK_T_NO' => '1143', 'VK_AMOUNT' => '.17', 'VK_CURR' => 'EUR', + 'VK_REC_ACC' => 'EE411010002050618003', 'VK_REC_NAME' => 'ÕILIS OÜ', + 'VK_SND_ACC' => 'EE541010010046155012', 'VK_SND_NAME' => 'TÕÄGER Leõpäöld¸´¨¦', + 'VK_REF' => '201501110000048', 'VK_MSG' => 'Invoice #20150111000004', 'VK_T_DATETIME' => response_time, + 'VK_ENCODING' => 'UTF-8', 'VK_LANG' => 'EST', 'VK_AUTO' => 'N' + } + } + + it 'should parse and verify the payment response from bank' do + signature = Ipizza::Util.sign(bank_key, nil, Ipizza::Util.mac_data_string(params, Ipizza::Response::PARAM_ORDER['1111'])) + Ipizza::Provider::Luminor.new.payment_response(params.merge('VK_MAC' => signature)).should be_valid + end + end + + describe '#authentication_request' do + before(:each) do + req_time = Time.now + Time.stub!(:now).and_return(req_time) + end + + it 'should sign the request' do + req = Ipizza::Provider::Luminor.new.authentication_request + params = { + 'VK_SERVICE' => '4011', + 'VK_VERSION' => '008', + 'VK_SND_ID' => Ipizza::Provider::Luminor.snd_id, + 'VK_RETURN' => Ipizza::Provider::Luminor.return_url, + 'VK_DATETIME' => Ipizza::Util.time_to_iso8601(Time.now), + 'VK_RID' => '', + 'VK_REPLY' => '3012' + } + signature = Ipizza::Util.sign(Ipizza::Provider::Luminor.file_key, Ipizza::Provider::Luminor.key_secret, Ipizza::Util.mac_data_string(params, Ipizza::Request::PARAM_ORDER['4011'])) + req.sign_params['VK_MAC'].should == signature + end + end + + describe '#authentication_response' do + let(:params) { + { + 'VK_SERVICE' => '3012', 'VK_VERSION' => '008', 'VK_USER' => 'dealer', 'VK_DATETIME' => response_time, + 'VK_SND_ID' => 'LUMINOR', 'VK_REC_ID' => 'sender', 'VK_USER_NAME' => 'TÕÄGER Leõpäöld¸´¨¦', 'VK_USER_ID' => '35511280268', + 'VK_COUNTRY' => 'EE', 'VK_OTHER' => '', 'VK_TOKEN' => '7', 'VK_RID' => '' + } + } + + it 'should parse and verify the authentication response from bank' do + signature = Ipizza::Util.sign(bank_key, nil, Ipizza::Util.mac_data_string(params, Ipizza::Response::PARAM_ORDER['3012'])) + Ipizza::Provider::Luminor.new.authentication_response(params.merge('VK_MAC' => signature)).should be_valid + end + end +end diff --git a/spec/ipizza/provider_spec.rb b/spec/ipizza/provider_spec.rb index d38987b..dbae315 100644 --- a/spec/ipizza/provider_spec.rb +++ b/spec/ipizza/provider_spec.rb @@ -9,11 +9,11 @@ it 'returns swedbank provider for "swedbank" attribute' do Ipizza::Provider.get('swedbank').should be_a(Ipizza::Provider::Swedbank) end - + it 'returns swedbank provider for "hp" attribute' do Ipizza::Provider.get('hp').should be_a(Ipizza::Provider::Swedbank) end - + it 'returns seb provider for "eyp" attribute' do Ipizza::Provider.get('eyp').should be_a(Ipizza::Provider::Seb) end @@ -22,6 +22,14 @@ Ipizza::Provider.get('seb').should be_a(Ipizza::Provider::Seb) end + it 'returns luminor provider for "luminor" attribute' do + Ipizza::Provider.get('luminor').should be_a(Ipizza::Provider::Luminor) + end + + it 'returns luminor provider for "testluminor" attribute' do + Ipizza::Provider.get('luminor').should be_a(Ipizza::Provider::Luminor) + end + it 'returns sampo provider for "sampo" attribute' do Ipizza::Provider.get('sampo').should be_a(Ipizza::Provider::Sampo) end @@ -33,11 +41,11 @@ it 'returns krediidipank provider for "krep" attribute' do Ipizza::Provider.get('krep').should be_a(Ipizza::Provider::Krediidipank) end - + it 'returns krediidipank provider for "krediidipank" attribute' do Ipizza::Provider.get('krediidipank').should be_a(Ipizza::Provider::Krediidipank) end - + it 'returns nordea provider for "nordea" attribute' do Ipizza::Provider.get('nordea').should be_a(Ipizza::Provider::Nordea) end