pesapal 1.5.4 → 1.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +12 -6
- data/.yardopts +12 -0
- data/CHANGELOG.md +14 -0
- data/{LICENSE.txt → LICENSE.md} +5 -2
- data/README.md +26 -250
- data/Rakefile +1 -1
- data/lib/generators/pesapal/install_generator.rb +3 -0
- data/lib/pesapal.rb +3 -3
- data/lib/pesapal/helper/details.rb +53 -0
- data/lib/pesapal/helper/post.rb +177 -0
- data/lib/pesapal/helper/status.rb +54 -0
- data/lib/pesapal/merchant.rb +398 -143
- data/lib/pesapal/oauth.rb +161 -122
- data/lib/pesapal/railtie.rb +4 -4
- data/lib/pesapal/version.rb +6 -1
- data/pesapal.gemspec +5 -3
- data/spec/pesapal_merchant_spec.rb +258 -6
- data/spec/spec_helper.rb +8 -0
- metadata +55 -35
- data/Gemfile.lock +0 -29
- data/lib/pesapal/merchant/details.rb +0 -24
- data/lib/pesapal/merchant/post.rb +0 -51
- data/lib/pesapal/merchant/status.rb +0 -29
data/lib/pesapal/oauth.rb
CHANGED
@@ -1,149 +1,182 @@
|
|
1
1
|
module Pesapal
|
2
|
-
|
2
|
+
# Supporting oAuth 1.0 methods. See [oAuth 1.0 spec][1] for details.
|
3
|
+
#
|
4
|
+
# [1]: http://oauth.net/core/1.0/
|
3
5
|
module Oauth
|
4
|
-
|
5
|
-
#
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
6
|
+
# Generate query string from a Hash.
|
7
|
+
#
|
8
|
+
# 1. Percent encode every key and value that will be signed
|
9
|
+
# 2. Sort the list of parameters alphabetically by encoded key
|
10
|
+
# 3. For each key/value pair
|
11
|
+
# * append the encoded key to the output string
|
12
|
+
# * append the '=' character to the output string
|
13
|
+
# * append the encoded value to the output string
|
14
|
+
# 4. If there are more key/value pairs remaining, append a '&' character
|
15
|
+
# to the output string
|
16
|
+
#
|
17
|
+
# The oauth spec says to sort lexicographically, which is the default
|
18
|
+
# alphabetical sort for many libraries. In case of two parameters with the
|
19
|
+
# same encoded key, the oauth spec says to continue sorting based on value.
|
20
|
+
#
|
21
|
+
# @param params [Hash] Hash of parameters.
|
22
|
+
#
|
23
|
+
# @return [String] valid valid parameter query string.
|
24
|
+
def self.generate_encoded_params_query_string(params = {})
|
22
25
|
queries = []
|
23
|
-
params.each
|
24
|
-
|
25
|
-
# parameters are sorted by name, using lexicographical byte value
|
26
|
-
# ordering
|
26
|
+
params.each { |k, v| queries.push "#{parameter_encode(k.to_s)}=#{parameter_encode(v.to_s)}" }
|
27
27
|
queries.sort!
|
28
|
-
|
29
28
|
queries.join('&')
|
30
29
|
end
|
31
30
|
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
31
|
+
# Generate an nonce
|
32
|
+
#
|
33
|
+
# > _The Consumer SHALL then generate a Nonce value that is unique for all
|
34
|
+
# > requests with that timestamp. A nonce is a random string, uniquely
|
35
|
+
# > generated for each request. The nonce allows the Service Provider to
|
36
|
+
# > verify that a request has never been made before and helps prevent
|
37
|
+
# > replay attacks when requests are made over a non-secure channel (such as
|
38
|
+
# > HTTP)._
|
39
|
+
#
|
40
|
+
# See [section 8 of the oAuth 1.0 spec][1]
|
41
|
+
#
|
42
|
+
# [1]: http://oauth.net/core/1.0/#nonce
|
43
|
+
#
|
44
|
+
# @param length [Integer] number of characters of the resulting nonce.
|
45
|
+
#
|
46
|
+
# @return [String] generated random nonce.
|
47
|
+
def self.generate_nonce(length)
|
42
48
|
chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789'
|
43
49
|
nonce = ''
|
44
50
|
length.times { nonce << chars[rand(chars.size)] }
|
45
|
-
|
46
51
|
"#{nonce}"
|
47
52
|
end
|
48
53
|
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
54
|
+
# Generate the oAuth signature using HMAC-SHA1 algorithm.
|
55
|
+
#
|
56
|
+
# The signature is calculated by passing the signature base string and
|
57
|
+
# signing key to the HMAC-SHA1 hashing algorithm. The output of the HMAC
|
58
|
+
# signing function is a binary string. this needs to be Base64 encoded to
|
59
|
+
# produce the signature string.
|
60
|
+
#
|
61
|
+
# For pesapal flow we don't have a token secret to we will set as nil and
|
62
|
+
# the appropriate action will be taken as per the oAuth spec. See
|
63
|
+
# {generate_signing_key} for details.
|
64
|
+
#
|
65
|
+
# @param http_method [String] the HTTP method.
|
66
|
+
#
|
67
|
+
# @param absolute_url [String] the absolute URL.
|
68
|
+
#
|
69
|
+
# @param params [Hash] URL parameters.
|
70
|
+
#
|
71
|
+
# @param consumer_secret [String] the consumer secret.
|
72
|
+
#
|
73
|
+
# @param token_secret [String] the token secret.
|
74
|
+
#
|
75
|
+
# @return [String] valid oAuth signature.
|
76
|
+
def self.generate_oauth_signature(http_method, absolute_url, params, consumer_secret, token_secret = nil)
|
62
77
|
digest = OpenSSL::Digest::Digest.new('sha1')
|
63
|
-
signature_base_string =
|
64
|
-
signing_key =
|
65
|
-
|
78
|
+
signature_base_string = generate_signature_base_string(http_method, absolute_url, params)
|
79
|
+
signing_key = generate_signing_key(consumer_secret, token_secret)
|
66
80
|
hmac = OpenSSL::HMAC.digest(digest, signing_key, signature_base_string)
|
67
81
|
Base64.encode64(hmac).chomp
|
68
82
|
end
|
69
83
|
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
84
|
+
# Generate query string from signable parameters Hash
|
85
|
+
#
|
86
|
+
# Same as {generate_encoded_params_query_string} but without
|
87
|
+
# `:oauth_signature` included in the parameters.
|
88
|
+
#
|
89
|
+
# @param params [Hash] Hash of parameters.
|
90
|
+
#
|
91
|
+
# @return [String] valid valid parameter query string.
|
92
|
+
def self.generate_signable_encoded_params_query_string(params = {})
|
75
93
|
params.delete(:oauth_signature)
|
76
|
-
|
77
|
-
self.generate_encoded_params_query_string params
|
94
|
+
generate_encoded_params_query_string params
|
78
95
|
end
|
79
96
|
|
80
|
-
#
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
97
|
+
# Generate an oAuth 1.0 signature base string.
|
98
|
+
#
|
99
|
+
# Three values collected so far must be joined to make a single string, from
|
100
|
+
# which the signature will be generated. This is called the signature base
|
101
|
+
# string. The signature base string should contain exactly 2 ampersand '&'
|
102
|
+
# characters. The percent '%' characters in the parameter string should be
|
103
|
+
# encoded as %25 in the signature base string.
|
104
|
+
#
|
105
|
+
# See [appendix A.5.1 of the oAuth 1.0 spec][1] for an example.
|
106
|
+
#
|
107
|
+
# [1]: http://oauth.net/core/1.0/#sig_base_example
|
108
|
+
#
|
109
|
+
# @param http_method [String] the HTTP method.
|
110
|
+
#
|
111
|
+
# @param absolute_url [String] the absolute URL.
|
112
|
+
#
|
113
|
+
# @param params [Hash] URL parameters.
|
114
|
+
#
|
115
|
+
# @return [String] valid signature base string.
|
116
|
+
def self.generate_signature_base_string(http_method, absolute_url, params)
|
86
117
|
|
87
118
|
# step 1: convert the http method to uppercase
|
88
119
|
http_method = http_method.upcase
|
89
120
|
|
90
121
|
# step 2: percent encode the url
|
91
|
-
url_encoded =
|
122
|
+
url_encoded = parameter_encode(normalized_request_uri(absolute_url))
|
92
123
|
|
93
124
|
# step 3: percent encode the parameter string
|
94
|
-
parameter_string_encoded =
|
95
|
-
|
96
|
-
# the signature base string should contain exactly 2 ampersand '&'
|
97
|
-
# characters. The percent '%' characters in the parameter string should be
|
98
|
-
# encoded as %25 in the signature base string
|
125
|
+
parameter_string_encoded = parameter_encode(generate_signable_encoded_params_query_string params)
|
99
126
|
|
100
127
|
"#{http_method}&#{url_encoded}&#{parameter_string_encoded}"
|
101
128
|
end
|
102
129
|
|
103
|
-
#
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
130
|
+
# Generate signing key
|
131
|
+
#
|
132
|
+
# The signing key is simply the percent encoded consumer secret, followed by
|
133
|
+
# an ampersand character '&', followed by the percent encoded token secret.
|
134
|
+
# Note that there are some flows, such as when obtaining a request token,
|
135
|
+
# where the token secret is not yet known. In this case, the signing key
|
136
|
+
# should consist of the percent encoded consumer secret followed by an
|
137
|
+
# ampersand character '&'.
|
138
|
+
#
|
139
|
+
# @param consumer_secret [String] the consumer secret.
|
140
|
+
#
|
141
|
+
# @param token_secret [String] the token secret.
|
142
|
+
#
|
143
|
+
# @return [String] valid signing key.
|
144
|
+
def self.generate_signing_key(consumer_secret, token_secret = nil)
|
145
|
+
consumer_secret_encoded = parameter_encode(consumer_secret)
|
146
|
+
token_secret_encoded = ''
|
147
|
+
token_secret_encoded = parameter_encode(token_secret) unless token_secret.nil?
|
122
148
|
|
123
149
|
"#{consumer_secret_encoded}&#{token_secret_encoded}"
|
124
150
|
end
|
125
151
|
|
126
|
-
#
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
152
|
+
# Construct normalized request absolute URL.
|
153
|
+
#
|
154
|
+
# > _The Signature Base String includes the request absolute URL, tying the
|
155
|
+
# > signature to a specific endpoint. The URL used in the Signature Base
|
156
|
+
# > String MUST include the scheme, authority, and path, and MUST exclude
|
157
|
+
# > the query and fragment as defined by [RFC3986] section 3._
|
158
|
+
#
|
159
|
+
# > _If the absolute request URL is not available to the Service Provider (it
|
160
|
+
# > is always available to the Consumer), it can be constructed by combining
|
161
|
+
# > the scheme being used, the HTTP Host header, and the relative HTTP
|
162
|
+
# > request URL. If the Host header is not available, the Service Provider
|
163
|
+
# > SHOULD use the host name communicated to the Consumer in the
|
164
|
+
# > documentation or other means._
|
165
|
+
#
|
166
|
+
# > _The Service Provider SHOULD document the form of URL used in the
|
167
|
+
# > Signature Base String to avoid ambiguity due to URL normalization.
|
168
|
+
# > Unless specified, URL scheme and authority MUST be lowercase and include
|
169
|
+
# > the port number; http default port 80 and https default port 443 MUST be
|
170
|
+
# > excluded._
|
171
|
+
#
|
172
|
+
# See [section 9.1.2 of the oAuth 1.0 spec][1]
|
173
|
+
#
|
174
|
+
# [1]: http://oauth.net/core/1.0/#anchor14
|
175
|
+
#
|
176
|
+
# @param absolute_url [String] URL to be normalized.
|
177
|
+
#
|
178
|
+
# @return [String] valid constructed URL as per the spec.
|
179
|
+
def self.normalized_request_uri(absolute_url)
|
147
180
|
u = URI.parse(absolute_url)
|
148
181
|
|
149
182
|
scheme = u.scheme.downcase
|
@@ -157,22 +190,28 @@ module Pesapal
|
|
157
190
|
"#{scheme}://#{host}#{port}#{path}"
|
158
191
|
end
|
159
192
|
|
160
|
-
#
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
193
|
+
# Encodes parameter name or values.
|
194
|
+
#
|
195
|
+
# > _All parameter names and values are escaped using the [RFC3986] percent-
|
196
|
+
# > encoding (%xx) mechanism. Characters not in the unreserved character set
|
197
|
+
# > ([RFC3986] section 2.3) MUST be encoded. Characters in the unreserved
|
198
|
+
# > character set MUST NOT be encoded. Hexadecimal characters in encodings
|
199
|
+
# > MUST be upper case. Text names and values MUST be encoded as UTF-8
|
200
|
+
# > octets before percent-encoding them per [RFC3629]._
|
201
|
+
#
|
202
|
+
# See [section 5.1 of the oAuth 1.0 spec][1]
|
203
|
+
#
|
204
|
+
# [1]: http://oauth.net/core/1.0/#encoding_parameters
|
205
|
+
#
|
206
|
+
# @param str [String] parameter name or value.
|
207
|
+
#
|
208
|
+
# @return [String] valid encoded result as per the spec.
|
209
|
+
def self.parameter_encode(str)
|
170
210
|
# reserved character regexp, per section 5.1
|
171
211
|
reserved_characters = /[^a-zA-Z0-9\-\.\_\~]/
|
172
|
-
|
173
212
|
# Apparently we can't force_encoding on a frozen string since that would modify it.
|
174
213
|
# What we can do is work with a copy
|
175
|
-
URI
|
214
|
+
URI.escape(str.dup.to_s.force_encoding(Encoding::UTF_8), reserved_characters)
|
176
215
|
end
|
177
216
|
end
|
178
217
|
end
|
data/lib/pesapal/railtie.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
module Pesapal
|
2
|
-
|
2
|
+
# Hooks Pesapal to extend Rails and/or modify the initialization process.
|
3
3
|
class Railtie < Rails::Railtie
|
4
|
-
|
4
|
+
# Loads pesapal credentials from initializer file depending on environment
|
5
|
+
# and fallback to default values if anything goes wrong.
|
5
6
|
initializer 'pesapal.load_credentials' do
|
6
|
-
|
7
7
|
path_to_yaml = "#{Rails.root}/config/pesapal.yml"
|
8
8
|
if File.exist?(path_to_yaml)
|
9
9
|
begin
|
10
|
-
config.pesapal_credentials = YAML
|
10
|
+
config.pesapal_credentials = YAML.load(IO.read(path_to_yaml))[Rails.env]
|
11
11
|
rescue Errno::ENOENT
|
12
12
|
logger.info('YAML configuration file couldn\'t be found.'); return
|
13
13
|
rescue Psych::SyntaxError
|
data/lib/pesapal/version.rb
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
# Make authenticated Pesapal API calls without the fuss! Easily post an order,
|
2
|
+
# query payment status and fetch payment details.
|
1
3
|
module Pesapal
|
2
|
-
|
4
|
+
# This gem's version (learn about [Semantic Versioning][1]).
|
5
|
+
#
|
6
|
+
# [1]: http://semver.org/
|
7
|
+
VERSION = '1.5.5'
|
3
8
|
end
|
data/pesapal.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ['j@kingori.co']
|
11
11
|
spec.description = 'Make authenticated Pesapal API calls without the fuss!'
|
12
12
|
spec.summary = 'Make authenticated Pesapal API calls without the fuss! Handles all the oAuth stuff abstracting any direct interaction with the API endpoints so that you can focus on what matters. Building awesome.'
|
13
|
-
spec.homepage = 'http://
|
13
|
+
spec.homepage = 'http://itsmrwave.github.io/pesapal-gem'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
@@ -18,9 +18,11 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_development_dependency 'bundler'
|
21
|
+
spec.add_development_dependency 'bundler'
|
22
|
+
spec.add_development_dependency 'coveralls'
|
22
23
|
spec.add_development_dependency 'rake'
|
23
|
-
spec.add_development_dependency 'rspec'
|
24
|
+
spec.add_development_dependency 'rspec'
|
25
|
+
spec.add_development_dependency 'webmock'
|
24
26
|
|
25
27
|
spec.add_dependency 'htmlentities'
|
26
28
|
end
|
@@ -44,6 +44,90 @@ describe Pesapal::Merchant do
|
|
44
44
|
})
|
45
45
|
end
|
46
46
|
end
|
47
|
+
|
48
|
+
describe '#generate_order_url' do
|
49
|
+
|
50
|
+
it 'generates iframe url string' do
|
51
|
+
@pesapal.order_details = { :amount => 1000,
|
52
|
+
:description => 'This is the description for the test transaction.',
|
53
|
+
:type => 'MERCHANT',
|
54
|
+
:reference => '111-222-333',
|
55
|
+
:first_name => 'Swaleh',
|
56
|
+
:last_name => 'Mdoe',
|
57
|
+
:email => 'test@example.com',
|
58
|
+
:phonenumber => '+254711000333',
|
59
|
+
:currency => 'KES'
|
60
|
+
}
|
61
|
+
expect(@pesapal.generate_order_url).to match /http:\/\/demo.pesapal.com\/API\/PostPesapalDirectOrderV4\?oauth_callback=.*oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_request_data=.*/
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#query_payment_status' do
|
66
|
+
|
67
|
+
it 'gets pending payment status' do
|
68
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
69
|
+
to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
|
70
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('PENDING')
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'gets completed payment status' do
|
74
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
75
|
+
to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
|
76
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('COMPLETED')
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'gets failed payment status' do
|
80
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
81
|
+
to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
|
82
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('FAILED')
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'gets invalid payment status' do
|
86
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
87
|
+
to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
|
88
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('INVALID')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#query_payment_details' do
|
93
|
+
|
94
|
+
it 'gets pending payment details' do
|
95
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentDetails\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
96
|
+
to_return(:status => 200, :body => 'pesapal_response_data=transaction_tracking_id,payment_method,payment_status,merchant_reference')
|
97
|
+
expect(@pesapal.query_payment_details('merchant_reference', 'transaction_tracking_id')).to eq({ :method => 'payment_method',
|
98
|
+
:status => 'payment_status',
|
99
|
+
:merchant_reference => 'merchant_reference',
|
100
|
+
:transaction_tracking_id => 'transaction_tracking_id'
|
101
|
+
})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#ipn_listener' do
|
106
|
+
|
107
|
+
it 'gets ipn response for pending status' do
|
108
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
109
|
+
to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
|
110
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'PENDING', :response => nil})
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'gets ipn response for completed status' do
|
114
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
115
|
+
to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
|
116
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'COMPLETED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'gets ipn response for failed status' do
|
120
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
121
|
+
to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
|
122
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'FAILED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'gets ipn response for invalid status' do
|
126
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
127
|
+
to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
|
128
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'INVALID', :response => nil})
|
129
|
+
end
|
130
|
+
end
|
47
131
|
end
|
48
132
|
|
49
133
|
context 'when mode is specified as development' do
|
@@ -64,9 +148,9 @@ describe Pesapal::Merchant do
|
|
64
148
|
|
65
149
|
it 'sets credentials' do
|
66
150
|
expect(@pesapal.config).to eq({ :callback_url => 'http://0.0.0.0:3000/pesapal/callback',
|
67
|
-
|
68
|
-
|
69
|
-
|
151
|
+
:consumer_key => '<YOUR_CONSUMER_KEY>',
|
152
|
+
:consumer_secret => '<YOUR_CONSUMER_SECRET>'
|
153
|
+
})
|
70
154
|
end
|
71
155
|
|
72
156
|
it 'sets order details' do
|
@@ -88,6 +172,90 @@ describe Pesapal::Merchant do
|
|
88
172
|
})
|
89
173
|
end
|
90
174
|
end
|
175
|
+
|
176
|
+
describe '#generate_order_url' do
|
177
|
+
|
178
|
+
it 'generates iframe url string' do
|
179
|
+
@pesapal.order_details = { :amount => 1000,
|
180
|
+
:description => 'This is the description for the test transaction.',
|
181
|
+
:type => 'MERCHANT',
|
182
|
+
:reference => '111-222-333',
|
183
|
+
:first_name => 'Swaleh',
|
184
|
+
:last_name => 'Mdoe',
|
185
|
+
:email => 'test@example.com',
|
186
|
+
:phonenumber => '+254711000333',
|
187
|
+
:currency => 'KES'
|
188
|
+
}
|
189
|
+
expect(@pesapal.generate_order_url).to match /http:\/\/demo.pesapal.com\/API\/PostPesapalDirectOrderV4\?oauth_callback=.*oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_request_data=.*/
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe '#query_payment_status' do
|
194
|
+
|
195
|
+
it 'gets pending payment status' do
|
196
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
197
|
+
to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
|
198
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('PENDING')
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'gets completed payment status' do
|
202
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
203
|
+
to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
|
204
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('COMPLETED')
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'gets failed payment status' do
|
208
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
209
|
+
to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
|
210
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('FAILED')
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'gets invalid payment status' do
|
214
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
215
|
+
to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
|
216
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('INVALID')
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe '#query_payment_details' do
|
221
|
+
|
222
|
+
it 'gets pending payment details' do
|
223
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentDetails\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
224
|
+
to_return(:status => 200, :body => 'pesapal_response_data=transaction_tracking_id,payment_method,payment_status,merchant_reference')
|
225
|
+
expect(@pesapal.query_payment_details('merchant_reference', 'transaction_tracking_id')).to eq({ :method => 'payment_method',
|
226
|
+
:status => 'payment_status',
|
227
|
+
:merchant_reference => 'merchant_reference',
|
228
|
+
:transaction_tracking_id => 'transaction_tracking_id'
|
229
|
+
})
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
describe '#ipn_listener' do
|
234
|
+
|
235
|
+
it 'gets ipn response for pending status' do
|
236
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
237
|
+
to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
|
238
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'PENDING', :response => nil})
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'gets ipn response for completed status' do
|
242
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
243
|
+
to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
|
244
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'COMPLETED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'gets ipn response for failed status' do
|
248
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
249
|
+
to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
|
250
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'FAILED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'gets ipn response for invalid status' do
|
254
|
+
stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
255
|
+
to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
|
256
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'INVALID', :response => nil})
|
257
|
+
end
|
258
|
+
end
|
91
259
|
end
|
92
260
|
|
93
261
|
context 'when mode is specified as production' do
|
@@ -108,9 +276,9 @@ describe Pesapal::Merchant do
|
|
108
276
|
|
109
277
|
it 'sets credentials' do
|
110
278
|
expect(@pesapal.config).to eq({ :callback_url => 'http://0.0.0.0:3000/pesapal/callback',
|
111
|
-
|
112
|
-
|
113
|
-
|
279
|
+
:consumer_key => '<YOUR_CONSUMER_KEY>',
|
280
|
+
:consumer_secret => '<YOUR_CONSUMER_SECRET>'
|
281
|
+
})
|
114
282
|
end
|
115
283
|
|
116
284
|
it 'sets order details' do
|
@@ -132,5 +300,89 @@ describe Pesapal::Merchant do
|
|
132
300
|
})
|
133
301
|
end
|
134
302
|
end
|
303
|
+
|
304
|
+
describe '#generate_order_url' do
|
305
|
+
|
306
|
+
it 'generates iframe url string' do
|
307
|
+
@pesapal.order_details = { :amount => 1000,
|
308
|
+
:description => 'This is the description for the test transaction.',
|
309
|
+
:type => 'MERCHANT',
|
310
|
+
:reference => '111-222-333',
|
311
|
+
:first_name => 'Swaleh',
|
312
|
+
:last_name => 'Mdoe',
|
313
|
+
:email => 'test@example.com',
|
314
|
+
:phonenumber => '+254711000333',
|
315
|
+
:currency => 'KES'
|
316
|
+
}
|
317
|
+
expect(@pesapal.generate_order_url).to match /https:\/\/www.pesapal.com\/API\/PostPesapalDirectOrderV4\?oauth_callback=.*oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_request_data=.*/
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
describe '#query_payment_status' do
|
322
|
+
|
323
|
+
it 'gets pending payment status' do
|
324
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
325
|
+
to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
|
326
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('PENDING')
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'gets completed payment status' do
|
330
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
331
|
+
to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
|
332
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('COMPLETED')
|
333
|
+
end
|
334
|
+
|
335
|
+
it 'gets failed payment status' do
|
336
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
337
|
+
to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
|
338
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('FAILED')
|
339
|
+
end
|
340
|
+
|
341
|
+
it 'gets invalid payment status' do
|
342
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
343
|
+
to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
|
344
|
+
expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('INVALID')
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
describe '#query_payment_details' do
|
349
|
+
|
350
|
+
it 'gets pending payment details' do
|
351
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentDetails\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
352
|
+
to_return(:status => 200, :body => 'pesapal_response_data=transaction_tracking_id,payment_method,payment_status,merchant_reference')
|
353
|
+
expect(@pesapal.query_payment_details('merchant_reference', 'transaction_tracking_id')).to eq({ :method => 'payment_method',
|
354
|
+
:status => 'payment_status',
|
355
|
+
:merchant_reference => 'merchant_reference',
|
356
|
+
:transaction_tracking_id => 'transaction_tracking_id'
|
357
|
+
})
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
describe '#ipn_listener' do
|
362
|
+
|
363
|
+
it 'gets ipn response for pending status' do
|
364
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
365
|
+
to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
|
366
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'PENDING', :response => nil})
|
367
|
+
end
|
368
|
+
|
369
|
+
it 'gets ipn response for completed status' do
|
370
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
371
|
+
to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
|
372
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'COMPLETED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
|
373
|
+
end
|
374
|
+
|
375
|
+
it 'gets ipn response for failed status' do
|
376
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
377
|
+
to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
|
378
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'FAILED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'gets ipn response for invalid status' do
|
382
|
+
stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
|
383
|
+
to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
|
384
|
+
expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'INVALID', :response => nil})
|
385
|
+
end
|
386
|
+
end
|
135
387
|
end
|
136
388
|
end
|