nordea-siirto 2.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.
@@ -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: []