nedbank_api 0.2.2
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +120 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/nedbank_api.rb +39 -0
- data/lib/nedbank_api/api_wrapper.rb +29 -0
- data/lib/nedbank_api/authentications_api.rb +57 -0
- data/lib/nedbank_api/exceptions.rb +5 -0
- data/lib/nedbank_api/models/base_model.rb +21 -0
- data/lib/nedbank_api/models/intent_token.rb +36 -0
- data/lib/nedbank_api/models/payment.rb +10 -0
- data/lib/nedbank_api/models/payment_submission.rb +6 -0
- data/lib/nedbank_api/payments_api.rb +37 -0
- data/lib/nedbank_api/services/client.rb +18 -0
- data/lib/nedbank_api/services/configuration.rb +18 -0
- data/lib/nedbank_api/services/http.rb +39 -0
- data/lib/nedbank_api/version.rb +3 -0
- data/nedbank_api.gemspec +34 -0
- data/spec/authorisations_api_spec.rb +51 -0
- data/spec/client_spec.rb +16 -0
- data/spec/factories/client.rb +17 -0
- data/spec/factories/payment.rb +0 -0
- data/spec/nedbank_api_spec.rb +12 -0
- data/spec/payments_api_spec.rb +51 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/authentication/post_auth_token.json +0 -0
- data/spec/support/payment/post_intent_request.json +41 -0
- data/spec/support/payment/post_intent_response.json +47 -0
- data/spec/support/payment/post_payment_submission_request.json +55 -0
- data/spec/support/payment/post_payment_submission_response.json +16 -0
- metadata +264 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module NedbankApi
|
2
|
+
module Models
|
3
|
+
class BaseModel < Delegator
|
4
|
+
attr_accessor :initialized_at
|
5
|
+
|
6
|
+
def initialize(obj)
|
7
|
+
super
|
8
|
+
@delegate_sd_obj = obj
|
9
|
+
self.initialized_at = Time.now
|
10
|
+
end
|
11
|
+
|
12
|
+
def __getobj__
|
13
|
+
@delegate_sd_obj
|
14
|
+
end
|
15
|
+
|
16
|
+
def __setobj__(obj)
|
17
|
+
@delegate_sd_obj = obj
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module NedbankApi
|
2
|
+
module Models
|
3
|
+
class IntentToken < BaseModel
|
4
|
+
ERRORS = {
|
5
|
+
token_expired: {
|
6
|
+
error: 'token_expired',
|
7
|
+
error_description: 'Intent Access Token has expired'
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
attr_accessor :token_expires_at,
|
12
|
+
:error,
|
13
|
+
:error_description
|
14
|
+
|
15
|
+
def token_expires_at
|
16
|
+
self.initialized_at + self.expires_in
|
17
|
+
end
|
18
|
+
|
19
|
+
def authenticated?
|
20
|
+
return false if self.access_token.nil?
|
21
|
+
!expired?
|
22
|
+
end
|
23
|
+
|
24
|
+
def expired?
|
25
|
+
return true if self.expires_in.nil?
|
26
|
+
raise Exceptions::TokenExpired if token_expires_at < Time.now
|
27
|
+
return false
|
28
|
+
|
29
|
+
rescue Exceptions::TokenExpired
|
30
|
+
self.error = ERRORS[:token_expired][:error]
|
31
|
+
self.error_description = ERRORS[:token_expired][:error_description ]
|
32
|
+
return true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module NedbankApi
|
2
|
+
class PaymentsApi < ApiWrapper
|
3
|
+
class << self
|
4
|
+
def create_intent(request_body: {})
|
5
|
+
http = Http.new(url: endpoint('/open-banking/payments'))
|
6
|
+
|
7
|
+
response = http.post(
|
8
|
+
headers: auth_headers,
|
9
|
+
body: request_body.to_json
|
10
|
+
)
|
11
|
+
|
12
|
+
return Models::Payment.new(json_to_object(response.body))
|
13
|
+
end
|
14
|
+
|
15
|
+
def submit_payment(request_body: {})
|
16
|
+
http = Http.new(url: endpoint('/open-banking/payment-submissions'))
|
17
|
+
|
18
|
+
response = http.post(
|
19
|
+
headers: auth_headers,
|
20
|
+
body: request_body.to_json
|
21
|
+
)
|
22
|
+
|
23
|
+
return Models::PaymentSubmission.new(json_to_object(response.body))
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_payment_submission(payment_submission_id:)
|
27
|
+
http = Http.new(url: endpoint('/open-banking/payment-submissions/' + payment_submission_id))
|
28
|
+
|
29
|
+
response = http.get(
|
30
|
+
headers: auth_headers
|
31
|
+
)
|
32
|
+
|
33
|
+
return Models::PaymentSubmission.new(json_to_object(response.body))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module NedbankApi
|
2
|
+
class Client
|
3
|
+
|
4
|
+
def initialize(client_id:, client_secret:, api_base: DEFAULT_API_BASE)
|
5
|
+
NedbankApi.client_id = client_id
|
6
|
+
NedbankApi.client_secret = client_secret
|
7
|
+
NedbankApi.api_base = api_base unless api_base.nil?
|
8
|
+
end
|
9
|
+
|
10
|
+
def authentication
|
11
|
+
@_authentication ||= AuthenticationsApi.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def payment
|
15
|
+
@_payment ||= PaymentsApi.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module NedbankApi
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
attr_accessor :client_id,
|
5
|
+
:client_secret,
|
6
|
+
:api_endpoint,
|
7
|
+
:oauth_redirect_url
|
8
|
+
|
9
|
+
API_ENDPOINT = 'https://api.nedbank.co.za/apimarket/sandbox'
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@client_id = nil
|
13
|
+
@client_secret = nil
|
14
|
+
@oauth_redirect_url = nil
|
15
|
+
@api_endpoint = API_ENDPOINT
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module NedbankApi
|
2
|
+
class Http
|
3
|
+
def initialize(url:)
|
4
|
+
@url = URI(url)
|
5
|
+
end
|
6
|
+
|
7
|
+
def net_http
|
8
|
+
@net_http ||= Net::HTTP.new(@url.host, @url.port).tap do |http|
|
9
|
+
http.use_ssl = true
|
10
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(body: {}, headers: {})
|
15
|
+
request = Net::HTTP::Get.new(@url)
|
16
|
+
|
17
|
+
headers.each do |key,value|
|
18
|
+
request[key] = value
|
19
|
+
end
|
20
|
+
|
21
|
+
request.body = body
|
22
|
+
response = net_http.request(request)
|
23
|
+
|
24
|
+
return response
|
25
|
+
end
|
26
|
+
def post(body: {}, headers: {})
|
27
|
+
request = Net::HTTP::Post.new(@url)
|
28
|
+
|
29
|
+
headers.each do |key,value|
|
30
|
+
request[key] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
request.body = body
|
34
|
+
response = net_http.request(request)
|
35
|
+
|
36
|
+
return response
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/nedbank_api.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'nedbank_api/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{nedbank_api}
|
8
|
+
s.version = NedbankApi::VERSION
|
9
|
+
s.date = %q{2019-09-11}
|
10
|
+
s.summary = %q{Nedbank API gem makes communicating with the Nedbank API quick and easy}
|
11
|
+
s.licenses = ['MIT']
|
12
|
+
s.authors = ['Jono Booth']
|
13
|
+
s.email = 'jonobth@gmail.com'
|
14
|
+
s.metadata = { 'source_code_uri' => 'https://github.com/jono-booth/nedbank_api' }
|
15
|
+
|
16
|
+
s.files = `git ls-files -z`.split("\x0")
|
17
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
s.test_files = s.files.grep(%r{^spec/})
|
19
|
+
s.require_paths = ['lib']
|
20
|
+
|
21
|
+
s.add_development_dependency 'bundler', '~> 1.5'
|
22
|
+
s.add_development_dependency 'rake', '~> 12.3'
|
23
|
+
s.add_development_dependency 'rspec', '~> 3.8'
|
24
|
+
s.add_development_dependency 'rspec-nc', '~> 0.3'
|
25
|
+
s.add_development_dependency 'webmock', '~> 3.7'
|
26
|
+
s.add_development_dependency 'factory_bot', '~> 5.0'
|
27
|
+
s.add_development_dependency 'guard', '~> 2.15'
|
28
|
+
s.add_development_dependency 'guard-rspec', '~> 4.7'
|
29
|
+
s.add_development_dependency 'pry', '~> 0.12'
|
30
|
+
s.add_development_dependency 'pry-remote', '~> 0.1'
|
31
|
+
s.add_development_dependency 'pry-nav', '~> 0.3'
|
32
|
+
s.add_development_dependency 'httplog', '~> 1.3'
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
RSpec.describe NedbankApi::AuthenticationsApi do
|
2
|
+
describe '.request_token_light' do
|
3
|
+
context 'a valid request' do
|
4
|
+
let(:response_body) { attributes_for :fake_client }
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
stub_request(:post, "https://api.nedbank.co.za/apimarket/sandbox/nboauth/oauth20/token").
|
8
|
+
to_return(status: 200, body: response_body.to_json, headers: {})
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'authenticates and sets the access token' do
|
12
|
+
token = NedbankApi::AuthenticationsApi.request_token_light
|
13
|
+
expect(NedbankApi.intent_token.access_token).to eq response_body[:access_token]
|
14
|
+
expect(token.authenticated?).to be true
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with an expired access token' do
|
18
|
+
it 'does not authenticate' do
|
19
|
+
token = NedbankApi::AuthenticationsApi.request_token_light
|
20
|
+
token.initialized_at = Time.now - 3599
|
21
|
+
expect(token.authenticated?).to be false
|
22
|
+
expect(token.error).to eq NedbankApi::Models::IntentToken::ERRORS[:token_expired][:error]
|
23
|
+
expect(token.error_description).to eq NedbankApi::Models::IntentToken::ERRORS[:token_expired][:error_description]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'given an error' do
|
29
|
+
let(:response_body) { attributes_for(:client_with_error, :invalid_client) }
|
30
|
+
|
31
|
+
before :each do
|
32
|
+
stub_request(:post, "https://api.nedbank.co.za/apimarket/sandbox/nboauth/oauth20/token").
|
33
|
+
to_return(status: 400, body: response_body.to_json, headers: {})
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'does not authenticate' do
|
37
|
+
token = NedbankApi::AuthenticationsApi.request_token_light
|
38
|
+
expect(token.authenticated?).to be false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '.authorise_url' do
|
44
|
+
let(:intent_id) { "GOODPEOPLEDRINKGOODBEER" }
|
45
|
+
|
46
|
+
it 'returns a payment auth url' do
|
47
|
+
authorisation_url = NedbankApi::AuthenticationsApi.authorisation_url(request_body: { intentid: intent_id })
|
48
|
+
expect(authorisation_url).to include "intentid=#{intent_id}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
RSpec.describe NedbankApi::Client do
|
2
|
+
describe '.initialize' do
|
3
|
+
|
4
|
+
it 'sets up the client' do
|
5
|
+
NedbankApi::Client.new(
|
6
|
+
client_id: 'CLIENTID',
|
7
|
+
client_secret: 'CLIENTSECRET',
|
8
|
+
api_base: 'APIBASE'
|
9
|
+
)
|
10
|
+
|
11
|
+
expect(NedbankApi.client_id).to eq 'CLIENTID'
|
12
|
+
expect(NedbankApi.client_secret).to eq 'CLIENTSECRET'
|
13
|
+
expect(NedbankApi.api_base).to eq 'APIBASE'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :fake_client, class: OpenStruct do
|
3
|
+
access_token { 'THISISBATCOUNTRY' }
|
4
|
+
client_id { 'NEVERTRUSTACOPINARAINCOAT' }
|
5
|
+
client_secret { 'TOOWEIRDTOLIVETORARETODIE' }
|
6
|
+
expires_in { 3599 }
|
7
|
+
api_url { 'https://api.nedbank.co.za/apimarket/sandbox' }
|
8
|
+
|
9
|
+
factory :client_with_error do
|
10
|
+
trait :invalid_client do
|
11
|
+
access_token { nil }
|
12
|
+
error_description { 'FBTOAU204E An invalid secret was provided for the client with identifier: [CLIENT_ID].' }
|
13
|
+
error { 'invalid_client' }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
File without changes
|
@@ -0,0 +1,12 @@
|
|
1
|
+
RSpec.describe NedbankApi do
|
2
|
+
it "has a version number" do
|
3
|
+
expect(NedbankApi::VERSION).not_to be nil
|
4
|
+
end
|
5
|
+
|
6
|
+
let(:client) { build :fake_client }
|
7
|
+
|
8
|
+
it "sets the client id" do
|
9
|
+
NedbankApi.client_id = client.client_id
|
10
|
+
expect(NedbankApi.client_id).to eq client.client_id
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
RSpec.describe NedbankApi::PaymentsApi do
|
2
|
+
describe '.create_intent' do
|
3
|
+
let(:request_body) { File.read('spec/support/payment/post_intent_request.json') }
|
4
|
+
let(:response_body) { File.read('spec/support/payment/post_intent_response.json') }
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
stub_request(:post, "https://api.nedbank.co.za/apimarket/sandbox/open-banking/payments").
|
8
|
+
to_return(status: 200, body: response_body)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns a payment object' do
|
12
|
+
payment = NedbankApi::PaymentsApi.create_intent(request_body: request_body)
|
13
|
+
|
14
|
+
expect(payment.Data.PaymentId).to eq JSON.parse(response_body)['Data']['PaymentId']
|
15
|
+
expect(payment.Data.Initiation.InstructedAmount.Amount).to eq JSON.parse(response_body)['Data']['Initiation']['InstructedAmount']['Amount']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.submit_payment' do
|
20
|
+
let(:request_body) { File.read('spec/support/payment/post_payment_submission_request.json') }
|
21
|
+
let(:response_body) { File.read('spec/support/payment/post_payment_submission_response.json') }
|
22
|
+
|
23
|
+
before :each do
|
24
|
+
stub_request(:post, "https://api.nedbank.co.za/apimarket/sandbox/open-banking/payment-submissions").
|
25
|
+
to_return(status: 200, body: response_body)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns a payment submission object' do
|
29
|
+
submission = NedbankApi::PaymentsApi.submit_payment(request_body: request_body)
|
30
|
+
expect(submission.PaymentSubmissionId).to eq "62820068622336"
|
31
|
+
expect(submission.PaymentId).to eq "8124568155717632"
|
32
|
+
expect(submission.Status).to eq "AcceptedSettlementInProcess"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '.get_payment_submission' do
|
37
|
+
let(:payment_submission_id) { "62820068622336" }
|
38
|
+
let(:response_body) { File.read('spec/support/payment/post_payment_submission_response.json') }
|
39
|
+
|
40
|
+
before :each do
|
41
|
+
stub_request(:get, "https://api.nedbank.co.za/apimarket/sandbox/open-banking/payment-submissions/#{payment_submission_id}").
|
42
|
+
to_return(status: 200, body: response_body)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns a payment submission object' do
|
46
|
+
submission = NedbankApi::PaymentsApi.get_payment_submission(payment_submission_id: payment_submission_id)
|
47
|
+
expect(submission.PaymentSubmissionId).to eq payment_submission_id
|
48
|
+
expect(submission.Status).to eq "AcceptedSettlementInProcess"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "nedbank_api"
|
3
|
+
require 'pry'
|
4
|
+
require 'webmock/rspec'
|
5
|
+
require 'factory_bot'
|
6
|
+
require 'httplog'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
# Enable flags like --only-failures and --next-failure
|
10
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
11
|
+
|
12
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
13
|
+
config.disable_monkey_patching!
|
14
|
+
|
15
|
+
config.expect_with :rspec do |c|
|
16
|
+
c.syntax = :expect
|
17
|
+
end
|
18
|
+
|
19
|
+
config.include FactoryBot::Syntax::Methods
|
20
|
+
|
21
|
+
config.before :all do
|
22
|
+
FactoryBot.find_definitions
|
23
|
+
end
|
24
|
+
|
25
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
26
|
+
end
|
27
|
+
|
28
|
+
HttpLog.configure do |config|
|
29
|
+
config.log_headers = false
|
30
|
+
config.color = :yellow
|
31
|
+
end
|
File without changes
|
@@ -0,0 +1,41 @@
|
|
1
|
+
{
|
2
|
+
"Data": {
|
3
|
+
"Initiation": {
|
4
|
+
"InstructionIdentification": "AAA4",
|
5
|
+
"EndToEndIdentification": "FRESCO.21302.GFX.24",
|
6
|
+
"InstructedAmount": {
|
7
|
+
"Amount": "55.60",
|
8
|
+
"Currency": "ZAR"
|
9
|
+
},
|
10
|
+
"CreditorAgent": {
|
11
|
+
"SchemeName": "ZASortCode",
|
12
|
+
"Identification": "470010"
|
13
|
+
},
|
14
|
+
"CreditorAccount": {
|
15
|
+
"SchemeName": "BBAN",
|
16
|
+
"Identification": "1028232942",
|
17
|
+
"Name": "ACME Inc",
|
18
|
+
"SecondaryIdentification": "0002"
|
19
|
+
},
|
20
|
+
"RemittanceInformation": {
|
21
|
+
"Unstructured": "Internal ops code 5120101",
|
22
|
+
"Reference": "FRESCO-101"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
},
|
26
|
+
"Risk": {
|
27
|
+
"PaymentContextCode": "Other",
|
28
|
+
"MerchantCategoryCode": "5967",
|
29
|
+
"MerchantCustomerIdentification": "053598653254",
|
30
|
+
"DeliveryAddress": {
|
31
|
+
"AddressLine": [
|
32
|
+
"Flat 7"
|
33
|
+
],
|
34
|
+
"StreetName": "Acacia Avenue",
|
35
|
+
"BuildingNumber": "27",
|
36
|
+
"PostCode": "GU31 2ZZ",
|
37
|
+
"TownName": "Sparsholt",
|
38
|
+
"Country": "UK"
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|