nordea-siirto 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7a303e3afcc99342e66813882aab9976f53b9a8b201cca342226779d0a81bcc9
4
+ data.tar.gz: '0914494f49ad84ab02fd00231c17b91cdb063920a147b57ae000a8093623f31b'
5
+ SHA512:
6
+ metadata.gz: c40ad8d7577fe6f02209344a8bf91b509a95cf0cf2543e3c8382fe06451fcba75cbfef69adbb7a88862fc48b6f0d51d12cbae0e390e2c91d53faf7eaf28f099d
7
+ data.tar.gz: fd2662e9219443cd1d4500f082669be73affaa8b1c9dcac61b8ee126326d56d8bdf6a33ed4d415f907a7ebe0f65ad18e67b962b30e3244f31bf4b3d617803713
@@ -0,0 +1,23 @@
1
+ # Dependencies
2
+ require 'net/http'
3
+ require 'iban'
4
+ require 'active_support'
5
+ require 'active_support/core_ext'
6
+
7
+ # Intended public interface of the gem
8
+ require 'nordea/siirto/siirto.rb'
9
+
10
+ # Implemented requests
11
+ require 'nordea/siirto/access_token.rb'
12
+ require 'nordea/siirto/lookup.rb'
13
+ require 'nordea/siirto/pay.rb'
14
+
15
+ # Implemented protocols
16
+ require 'nordea/siirto/protocols/base.rb'
17
+ require 'nordea/siirto/protocols/net_http.rb'
18
+ require 'nordea/siirto/protocols/curl.rb'
19
+
20
+ # Utility classes
21
+ require 'nordea/siirto/errors.rb'
22
+ require 'nordea/siirto/request.rb'
23
+ require 'nordea/siirto/response.rb'
@@ -0,0 +1,70 @@
1
+ module Nordea
2
+ module Siirto
3
+ # Responsible for fetching access token from the server,
4
+ # and memoizing it.
5
+ module AccessToken
6
+ # Store current access token into REDIS
7
+ KEY = 'Nordea::Siirto::AccessToken'.freeze
8
+ EXPIRATION_BUFFER = 20 # seconds, arbitrary
9
+ MUTEX = Mutex.new
10
+
11
+ module_function
12
+
13
+ # Fetches access token from server if previous token has expired
14
+ # Memoizes token, and sets expiration time, with some buffer.
15
+ # @return [String]
16
+ # rubocop:disable MethodLength
17
+ def access_token
18
+ # Synchronization is needed, otherwise race condition may ensue:
19
+ # Let's assume token has expired, and two threads ask for a new token.
20
+ # Both proceed to fetch the token from remote server, both return,
21
+ # but the first token is no longer valid.
22
+ MUTEX.synchronize do
23
+ token = Nordea::Siirto.redis.get(KEY)
24
+ return token if token
25
+
26
+ Nordea::Siirto.log('Requesting new access token...')
27
+ payload = response.body
28
+
29
+ token = payload['access_token']
30
+ expires_in = payload['expires_in'] - EXPIRATION_BUFFER
31
+ Nordea::Siirto.redis.set(KEY, token)
32
+ Nordea::Siirto.redis.expire(KEY, expires_in)
33
+
34
+ token
35
+ end
36
+ end
37
+ # rubocop:enable MethodLength
38
+
39
+ # @return [URI::HTTPS]
40
+ def uri
41
+ @uri ||= URI.parse("#{Nordea::Siirto.endpoint}/auth")
42
+ end
43
+
44
+ # @return Nordea::Siirto::Request
45
+ # rubocop:disable MethodLength
46
+ def request
47
+ request = Nordea::Siirto::Request.new
48
+ request.uri = uri
49
+ request.method = 'POST'
50
+ request.headers = {
51
+ 'Accept' => 'application/json',
52
+ 'Content-Type' => 'application/x-www-form-urlencoded'
53
+ }
54
+ request.body = {
55
+ grant_type: 'password',
56
+ username: Nordea::Siirto.username,
57
+ password: Nordea::Siirto.api_token,
58
+ client_id: Nordea::Siirto.username
59
+ }.to_query
60
+ request
61
+ end
62
+ # rubocop:enable MethodLength
63
+
64
+ # @return [Nordea::Siirto::Response]
65
+ def response
66
+ Nordea::Siirto.protocol.send!(request)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,13 @@
1
+ module Nordea
2
+ # Gem specific errors
3
+ module Siirto
4
+ class InitializationError < StandardError; end
5
+
6
+ # Errors used by Pay module
7
+ module Pay
8
+ class InvalidIBAN < ArgumentError; end
9
+ class InvalidPayload < ArgumentError; end
10
+ class MissingLookupId < StandardError; end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ module Nordea
2
+ module Siirto
3
+ # Fetches unique LookupId from Nordea server.
4
+ # LookupId is required to make a payment request.
5
+ module Lookup
6
+ module_function
7
+
8
+ # @return [Hash]
9
+ def lookup
10
+ response = Nordea::Siirto.protocol.send!(request)
11
+ response.body
12
+ end
13
+
14
+ # @return [URI::HTTPS]
15
+ def uri
16
+ @uri ||= URI.parse("#{Nordea::Siirto.endpoint}/lookup/uuid")
17
+ end
18
+
19
+ # @return [Nordea::Siirto::Request]
20
+ def request
21
+ request = Nordea::Siirto::Request.new
22
+ request.uri = uri
23
+ request.method = 'GET'
24
+ request.headers = {
25
+ 'Accept' => 'application/json',
26
+ 'Authorization' => "Bearer #{AccessToken.access_token}"
27
+ }
28
+ request
29
+ end
30
+
31
+ # @return [Nordea::Siirto::Response]
32
+ def response
33
+ Nordea::Siirto.protocol.send!(request)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,102 @@
1
+ module Nordea
2
+ module Siirto
3
+ # Implements Nordea Siirto IBAN payments.
4
+ module Pay
5
+ # Accepted parameters
6
+ PARAMS = {
7
+ required: [:amount, :currency, :bene_account_number],
8
+ person: [:bene_first_names, :bene_last_name],
9
+ company: [:bene_company_name],
10
+ optional: [:fallback_payment, :reference_number, :payment_message,
11
+ :ultimate_bene_ref_name, :beneficiary_minimum_age,
12
+ :beneficiary_identifier]
13
+ }.freeze
14
+
15
+ module_function
16
+
17
+ # @param [Hash] See README
18
+ # @return [Nordea::Siirto::Response]
19
+ def pay(params) # :nodoc:
20
+ raise InvalidPayload, params.inspect unless valid_payload?(params)
21
+ raise InvalidIBAN, params.inspect unless valid_iban?(params)
22
+
23
+ Nordea::Siirto.protocol.send!(request(params))
24
+ end
25
+
26
+ # @return [URI::HTTPS]
27
+ def uri # :nodoc:
28
+ @uri ||= URI.parse("#{Nordea::Siirto.endpoint}/payment/pay")
29
+ end
30
+
31
+ # @param [Hash]
32
+ # @return [Nordea::Siirto::Request]
33
+ # rubocop:disable MethodLength
34
+ def request(params)
35
+ request = Nordea::Siirto::Request.new
36
+ request.uri = uri
37
+ request.method = 'POST'
38
+ request.headers = {
39
+ 'Accept' => 'application/json',
40
+ 'Content-type' => 'application/json',
41
+ 'Authorization' => "Bearer #{AccessToken.access_token}"
42
+ }
43
+ request.body = format_params(params).to_json
44
+ Nordea::Siirto.log("Body: #{request.body}")
45
+ request
46
+ end
47
+ # rubocop:enable MethodLength
48
+
49
+ # @param [Hash]
50
+ # @return [Hash]
51
+ def format_params(params) # :nodoc:
52
+ hash = params.map do |key, val|
53
+ # dromedar case required
54
+ str = key.to_s.camelize
55
+ str[0] = str[0].downcase
56
+ { str => val }
57
+ end.reduce(&:merge)
58
+
59
+ # unique lookupId is needed for each payment request
60
+ lookup_id = Lookup.lookup.slice('lookupId')
61
+ raise MissingLookupId unless lookup_id.present?
62
+
63
+ hash.merge(lookup_id)
64
+ end
65
+
66
+
67
+ # @param [Hash]
68
+ # @return [Boolean]
69
+ def valid_iban?(params)
70
+ # It makes sense to check IBAN validity and bank compatibility before
71
+ # sending request
72
+ iban = Iban.new(params[:bene_account_number])
73
+ return false unless iban.validate
74
+
75
+ ALLOWED_BIC.include?(iban.bic)
76
+ end
77
+
78
+ # @param [Hash]
79
+ # @return [Boolean]
80
+ # rubocop:disable AbcSize,LineLength
81
+ def valid_payload?(params)
82
+ return false unless params.is_a?(Hash)
83
+
84
+ # convenience lambda for testing conditions
85
+ params_present = lambda do |key|
86
+ (params.keys & PARAMS[key]).size == PARAMS[key].size
87
+ end
88
+
89
+ # required params present
90
+ return false unless params_present.call(:required)
91
+
92
+ # either person or company params present
93
+ return false if params_present.call(:person) && params_present.call(:company)
94
+ return false unless params_present.call(:person) || params_present.call(:company)
95
+
96
+ # must not contain other params than those listed above
97
+ (params.keys - PARAMS.values.flatten).size.zero?
98
+ end
99
+ # rubocop:enable AbcSize,LineLength
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,68 @@
1
+ module Nordea
2
+ module Siirto
3
+ # NOTE: NOT COVERED BY TEST SET
4
+ module Protocols
5
+ # This class may be used as a base class for protocol implementations.
6
+ # Sub-classes should implement :send_request and :parse_response methods
7
+ # correctly.
8
+ #
9
+ # Gem expects protocol implementation to respond to :send! method, which
10
+ # takes in a generic Siirto request object, and returns a generic Siirto
11
+ # response object.
12
+ class Base
13
+ # Public interface of protocol implementations
14
+ # @param [Nordea::Siirto::Request]
15
+ # @return [Nordea::Siirto::Response]
16
+ # rubocop:disable MethodLength,AbcSize,LineLength
17
+ def send!(request)
18
+ uri = request.uri
19
+ Nordea::Siirto.log("Sending request to: #{uri}")
20
+
21
+ # Send request
22
+ begin
23
+ protocol_response = send_request(request)
24
+ rescue StandardError => e
25
+ Nordea::Siirto.log("Failed to send request: #{request.inspect} #{e.message}")
26
+ raise
27
+ end
28
+
29
+ # Parse response
30
+ begin
31
+ response = parse_response(protocol_response)
32
+ rescue StandardError => e
33
+ Nordea::Siirto.log("Failed to parse response: #{protocol_response.inspect} #{e.message}")
34
+ raise
35
+ end
36
+
37
+ # Log response
38
+ message = "Server responds: #{response.message}"
39
+ message << " #{response.code}"
40
+ unless response.body['access_token'] # do not log token
41
+ message << " #{response.body}"
42
+ end
43
+ Nordea::Siirto.log(message)
44
+
45
+ response
46
+ end
47
+ # rubocop:enable MethodLength,AbcSize,LineLength
48
+
49
+ private
50
+
51
+ # Sub-class must implement
52
+ # @param [Nordea::Siirto::Request]
53
+ # @return [Object] Protocol-specific response object
54
+ def send_request(request)
55
+ raise NotImplementedError, 'Sub-class must implement'
56
+ end
57
+
58
+ # Sub-class must implement
59
+ # @params [Object] Protocol-specific response object
60
+ # @return [Nordea::Siirto::Response]
61
+ def parse_response(protocol_response)
62
+ raise NotImplementedError, 'Sub-class must implement'
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
@@ -0,0 +1,66 @@
1
+ module Nordea
2
+ module Siirto
3
+ # NOTE: NOT COVERED BY TEST SET
4
+ module Protocols
5
+ # Implements communication with Nordea server using command line cURL.
6
+ # Provided as an alternative protocol for net/http, as some Ruby
7
+ # implementations fail to complete SSL Handshake with Nordea servers.
8
+ # At least JRuby 1.9.17 falls in this category.
9
+ #
10
+ # WARNING: We cannot guarantee what Nordea::Siirto::Response#code
11
+ # will contain. With legacy JRuby, a better way (next to upgrading)
12
+ # would be to wrap sufficiently modern and robust Java HTTP library.
13
+ # That way server responses would be more reliable.
14
+ class Curl < Base
15
+ private
16
+
17
+ # Parses generic Siirto request and returns curl command string
18
+ # @param [Nordea::Siirto::Request]
19
+ # @return [String]
20
+ def create_request(siirto_request)
21
+ # i - show information, not just response body
22
+ # s - hide statusbar, error information
23
+ request = "curl -X #{siirto_request.method} -is"
24
+ siirto_request.headers.each do |header, value|
25
+ request << " --header '#{header}: #{value}'"
26
+ end
27
+ if (body = siirto_request.body).present?
28
+ request << " --data '#{body}'"
29
+ end
30
+ request << " #{siirto_request.uri}"
31
+ request
32
+ end
33
+
34
+ # Makes the actual request
35
+ # @param [Nordea::Siirto::Request]
36
+ # @return [Net::HTTPRequest]
37
+ def send_request(siirto_request)
38
+ request = create_request(siirto_request)
39
+ IO.popen(request)
40
+ end
41
+
42
+ # Parses curl response string and returns a generic Siirto response
43
+ # @params [String]
44
+ # @return [Nordea::Siirto::Response]
45
+ def parse_response(curl_response)
46
+ lines = curl_response.readlines
47
+ # Absence of network connection
48
+ raise IOError, 'Curl response empty' if lines.blank?
49
+
50
+ code = lines.first.split(' ').last
51
+ body = JSON.parse(lines.last)
52
+
53
+ response = Nordea::Siirto::Response.new
54
+ response.code = code
55
+ # Nordea server responds to curl in HTTP/2, and apparently in HTTP/2
56
+ # there is no standard way to return HTTP status message (e.g. OK, Not
57
+ # Found), unlike in HTTP/1.1. We would need to either force HTTP/1.1
58
+ # or map status codes to messages here.
59
+ response.message = ''
60
+ response.body = body
61
+ response
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,52 @@
1
+ module Nordea
2
+ module Siirto
3
+ # NOTE: NOT COVERED BY TEST SET
4
+ module Protocols
5
+ # Implements communication with Nordea server using Ruby's standard
6
+ # net/http library
7
+ class NetHttp < Base
8
+ private
9
+
10
+ # Creates protocol-specific request from generic Siirto request
11
+ # @param [Nordea::Siirto::Request]
12
+ # @return [Net::HTTPRequest]
13
+ def create_request(siirto_request)
14
+ # Extract data
15
+ klass = "Net::HTTP::#{siirto_request.method.capitalize}".constantize
16
+ uri = siirto_request.uri.request_uri
17
+ body = siirto_request.body
18
+ headers = siirto_request.headers
19
+
20
+ # Create new Request object
21
+ request = klass.new(uri)
22
+ headers.each do |header, value|
23
+ request[header] = value
24
+ end
25
+ request.body = body if body.present?
26
+ request
27
+ end
28
+
29
+ # Makes the actual request
30
+ # @param [Nordea::Siirto::Request]
31
+ # @return [Net::HTTPRequest]
32
+ def send_request(siirto_request)
33
+ request = create_request(siirto_request)
34
+ uri = siirto_request.uri
35
+ http = Net::HTTP.new(uri.host, uri.port)
36
+ http.use_ssl = true if uri.port == 443
37
+ http.request(request)
38
+ end
39
+
40
+ # Parses NET::HTTPResponse into a generic Siirto response
41
+ # @param [Net::HTTPResponse]
42
+ # @return [Nordea::Siirto::Response]
43
+ def parse_response(http_response)
44
+ response = Nordea::Siirto::Response.new
45
+ response.code = http_response.code
46
+ response.body = JSON.parse(http_response.body)
47
+ response.message = http_response.message
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ module Nordea
2
+ module Siirto
3
+ # Generic data class which Protocol implementations can use
4
+ class Request
5
+ attr_accessor :uri, :body, :headers, :method
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Nordea
2
+ module Siirto
3
+ # Generic data class which Protocol implementations should return
4
+ class Response
5
+ attr_accessor :code, :message, :body
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,132 @@
1
+ module Nordea
2
+ # This module is intended as the sole public API of this gem.
3
+ #
4
+ # AccessToken and Lookup requests are needed for actual processing
5
+ # requests, such as Pay, to work. Client should not need to call them
6
+ # directly, and therefore they are not directly callable from this module.
7
+ #
8
+ # A method handle for each request meant to be called directly by client,
9
+ # should be included in this API.
10
+ module Siirto
11
+ # Nordea endpoints
12
+ ENDPOINT = {
13
+ prod: 'https://merchant.mobilewalletservices.nordea.com',
14
+ test: 'https://merchant.trescomas.express'
15
+ }.freeze
16
+
17
+ # Only Nordea and OP supported at the moment
18
+ ALLOWED_BIC = %w[NDEAFIHH OKOYFIHH].freeze
19
+
20
+ # Singleton features implemented as such
21
+ class << self
22
+ # Mandatory params: Client must provide these at setup
23
+ attr_reader :server, :username, :api_token
24
+
25
+ # Optional params: Client may provide these at setup
26
+ attr_reader :logger, :tag, :redis, :protocol
27
+
28
+ # Make sure initialization is thread safe
29
+ MUTEX = Mutex.new
30
+
31
+ # Client must initialize Nordea::Siirto module before calling other
32
+ # methods.
33
+ #
34
+ # @params opts [Hash] See README for details
35
+ # @raise [Nordea::Siirto::InitializationError]
36
+ # @return [Boolean]
37
+ # rubocop:disable MethodLength
38
+ def setup(opts)
39
+ MUTEX.synchronize do
40
+ allow_initialize?(opts)
41
+
42
+ # Initialize
43
+ opts.each do |key, val|
44
+ attr = "@#{key}".to_sym
45
+ instance_variable_set(attr, val)
46
+ end
47
+
48
+ # Client can inject logger of choice
49
+ @logger ||= Rails.logger
50
+
51
+ # Client can inject logging tag
52
+ @tag ||= 'Nordea::Siirto --'
53
+
54
+ # Client can inject REDIS instance of choice, or adapter.
55
+ @redis ||= REDIS
56
+
57
+ # Client can inject another Protocol
58
+ @protocol ||= Protocols::NetHttp.new
59
+
60
+ # Initialization complete
61
+ log('Initialized!')
62
+ true
63
+ end
64
+ end
65
+ # rubocop:enable MethodLength
66
+
67
+ # Checks if gem is already initialized with required parameters.
68
+ # @return [Boolean]
69
+ def initialized?
70
+ server.present? && username.present? && api_token.present?
71
+ end
72
+
73
+ # 8.2. Send a payment using IBAN account number
74
+ # POST /payment/pay
75
+ #
76
+ # @param payload [Hash] See README
77
+ # @raise [Nordea::Siirto::Pay::InvalidIBAN]
78
+ # @raise [Nordea::Siirto::Pay::InvalidPayload]
79
+ # @return [Nordea::Siirto::Response]
80
+ def pay(payload)
81
+ Pay.pay(payload)
82
+ end
83
+
84
+ # Convenience method for requests
85
+ # @return [String]
86
+ def endpoint
87
+ ENDPOINT[server]
88
+ end
89
+
90
+ # Convenience method for requests
91
+ # @param msg [String]
92
+ def log(msg)
93
+ logger.info("#{tag} #{msg}")
94
+ end
95
+
96
+ private
97
+
98
+ # Error messages
99
+ ERROR = {
100
+ already_initialized: 'Nordea::Siirto is already initialized.',
101
+ missing_args: 'Invalid or missing arguments. Client must provide
102
+ parameter hash with the following keys: :server (either :prod or
103
+ :test), :username, :api_token.',
104
+ invalid_logger: 'Logger must respond to :info method.',
105
+ invalid_protocol: 'Protocol must respond to :send! method'
106
+ }.freeze
107
+
108
+ # Checks that module has not been previously initialized, and
109
+ # that arguments are more or less acceptable.
110
+ # @raise [Nordea::Siirto::InitializationError]
111
+ # rubocop:disable AbcSize,CyclomaticComplexity,GuardClause
112
+ def allow_initialize?(opts)
113
+ raise InitializationError, ERROR[:already_initialized] if initialized?
114
+ raise InitializationError, ERROR[:missing_args] if missing_args?(opts)
115
+ if opts[:logger] && !opts[:logger].respond_to?(:info)
116
+ raise InitializationError, ERROR[:invalid_logger]
117
+ end
118
+ if opts[:protocol] && !opts[:protocol].respond_to?(:send!)
119
+ raise InitializationError, ERROR[:invalid_protocol]
120
+ end
121
+ end
122
+ # rubocop:enable AbcSize,CyclomaticComplexity,GuardClause
123
+
124
+ # Checks that required parameters are present.
125
+ # @return [Boolean]
126
+ def missing_args?(opts)
127
+ !(opts[:server] && opts[:username] && opts[:api_token] &&
128
+ ENDPOINT.keys.include?(opts[:server].to_sym))
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,5 @@
1
+ module Nordea
2
+ module Siirto
3
+ VERSION = '2.0.1'.freeze
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nordea-siirto
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Matilda Smeds
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-09-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2
14
+ Nordea::Siirto implements requests according to Nordea Siirto protocol,
15
+ which enables real time payments for select Finnish bank accounts
16
+ email: foss@aavasoftware.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/nordea/siirto.rb
22
+ - lib/nordea/siirto/access_token.rb
23
+ - lib/nordea/siirto/errors.rb
24
+ - lib/nordea/siirto/lookup.rb
25
+ - lib/nordea/siirto/pay.rb
26
+ - lib/nordea/siirto/protocols/base.rb
27
+ - lib/nordea/siirto/protocols/curl.rb
28
+ - lib/nordea/siirto/protocols/net_http.rb
29
+ - lib/nordea/siirto/request.rb
30
+ - lib/nordea/siirto/response.rb
31
+ - lib/nordea/siirto/siirto.rb
32
+ - lib/nordea/siirto/version.rb
33
+ homepage:
34
+ licenses:
35
+ - MIT
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.7.9
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Nordea Siirto requests
57
+ test_files: []