pesapal 1.2.1 → 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +14 -2
- data/README.md +7 -7
- data/lib/generators/templates/README.md +6 -6
- data/lib/generators/templates/pesapal.yml +6 -6
- data/lib/pesapal/merchant.rb +164 -161
- data/lib/pesapal/merchant/details.rb +16 -16
- data/lib/pesapal/merchant/post.rb +48 -48
- data/lib/pesapal/merchant/status.rb +19 -19
- data/lib/pesapal/oauth.rb +134 -134
- data/lib/pesapal/version.rb +1 -1
- data/pesapal.gemspec +17 -17
- metadata +19 -11
- checksums.yaml +0 -7
data/CHANGELOG.md
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
Changelog
|
2
2
|
=========
|
3
3
|
|
4
|
+
v1.2.2
|
5
|
+
------
|
6
|
+
|
7
|
+
* Fix `can't modify frozen String` error. Apparently we can't force_encoding on
|
8
|
+
a frozen string since that would modify it.
|
9
|
+
|
4
10
|
v1.2.1
|
5
11
|
------
|
6
12
|
|
7
|
-
* Fix severe bug caused by buggy initializer
|
13
|
+
* Fix severe bug caused by buggy (& poorly designed) initializer, which caused
|
14
|
+
problem for some [as evidenced here on SO][1]
|
15
|
+
|
16
|
+
_Ps: Run the install generator (as explained in the docs), if there are any
|
17
|
+
conflicts, overwrite the initializer only! Or you could just delete the
|
18
|
+
`config/initializer/pesapal.rb` file and run the generator. Whichever way suits
|
19
|
+
you._
|
8
20
|
|
9
|
-
|
21
|
+
[1]: http://stackoverflow.com/questions/19642460/rails-you-cannot-have-more-than-one-railsapplication-runtimeerror
|
10
22
|
|
11
23
|
v1.2.0
|
12
24
|
------
|
data/README.md
CHANGED
@@ -116,14 +116,14 @@ them appropriately.
|
|
116
116
|
|
117
117
|
```yaml
|
118
118
|
development:
|
119
|
-
|
120
|
-
|
121
|
-
|
119
|
+
callback_url: 'http://0.0.0.0:3000/pesapal/callback'
|
120
|
+
consumer_key: '<YOUR_CONSUMER_KEY>'
|
121
|
+
consumer_secret: '<YOUR_CONSUMER_SECRET>'
|
122
122
|
|
123
123
|
production:
|
124
|
-
|
125
|
-
|
126
|
-
|
124
|
+
callback_url: 'http://1.2.3.4:3000/pesapal/callback'
|
125
|
+
consumer_key: '<YOUR_CONSUMER_KEY>'
|
126
|
+
consumer_secret: '<YOUR_CONSUMER_SECRET>'
|
127
127
|
```
|
128
128
|
|
129
129
|
_Ps: Immediately after initializing the Pesapal object, some people might find
|
@@ -145,7 +145,7 @@ e.g. the phonenumber.
|
|
145
145
|
pesapal.order_details = { :amount => 1000,
|
146
146
|
:description => 'this is the transaction description',
|
147
147
|
:type => 'MERCHANT',
|
148
|
-
:reference => 808-707-606,
|
148
|
+
:reference => '808-707-606',
|
149
149
|
:first_name => 'Swaleh',
|
150
150
|
:last_name => 'Mdoe',
|
151
151
|
:email => 'user@example.com',
|
@@ -7,14 +7,14 @@ the values appropriately.
|
|
7
7
|
|
8
8
|
```
|
9
9
|
development:
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
callback_url: 'http://0.0.0.0:3000/pesapal/callback'
|
11
|
+
consumer_key: '<YOUR_CONSUMER_KEY>'
|
12
|
+
consumer_secret: '<YOUR_CONSUMER_SECRET>'
|
13
13
|
|
14
14
|
production:
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
callback_url: 'http://1.2.3.4:3000/pesapal/callback'
|
16
|
+
consumer_key: '<YOUR_CONSUMER_KEY>'
|
17
|
+
consumer_secret: '<YOUR_CONSUMER_SECRET>'
|
18
18
|
```
|
19
19
|
|
20
20
|
The config file can be found at `#{Rails.root}/config/pesapal.yml`.
|
@@ -1,9 +1,9 @@
|
|
1
1
|
development:
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
callback_url: 'http://0.0.0.0:3000/pesapal/callback'
|
3
|
+
consumer_key: '<YOUR_CONSUMER_KEY>'
|
4
|
+
consumer_secret: '<YOUR_CONSUMER_SECRET>'
|
5
5
|
|
6
6
|
production:
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
callback_url: 'http://1.2.3.4:3000/pesapal/callback'
|
8
|
+
consumer_key: '<YOUR_CONSUMER_KEY>'
|
9
|
+
consumer_secret: '<YOUR_CONSUMER_SECRET>'
|
data/lib/pesapal/merchant.rb
CHANGED
@@ -1,223 +1,226 @@
|
|
1
1
|
module Pesapal
|
2
2
|
|
3
|
-
|
3
|
+
class Merchant
|
4
4
|
|
5
|
-
|
5
|
+
attr_accessor :config, :order_details
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
def config
|
8
|
+
@config ||= {}
|
9
|
+
end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def order_details
|
12
|
+
@order_details ||= {}
|
13
|
+
end
|
14
14
|
|
15
|
-
|
15
|
+
private
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
def api_domain
|
18
|
+
@api_domain
|
19
|
+
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
def api_endpoints
|
22
|
+
@api_endpoints
|
23
|
+
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
def mode
|
26
|
+
@mode
|
27
|
+
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
def params
|
30
|
+
@params
|
31
|
+
end
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
def post_xml
|
34
|
+
@post_xml
|
35
|
+
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
def token_secret
|
38
|
+
@token_secret
|
39
|
+
end
|
40
40
|
|
41
|
-
|
41
|
+
public
|
42
42
|
|
43
|
-
|
44
|
-
|
43
|
+
# constructor
|
44
|
+
def initialize(mode = :development, path_to_file = nil)
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
46
|
+
# initialize
|
47
|
+
@params = nil
|
48
|
+
@post_xml = nil
|
49
|
+
@token_secret = nil
|
50
50
|
|
51
|
-
|
51
|
+
set_mode mode
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
# set the credentials if we have specified a path from which we
|
54
|
+
# will access a YAML file with the configurations
|
55
|
+
unless path_to_file.nil?
|
56
|
+
set_configuration_from_yaml path_to_file
|
57
|
+
end
|
58
58
|
|
59
|
-
|
59
|
+
end
|
60
60
|
|
61
|
-
|
62
|
-
|
61
|
+
# generate pesapal order url (often iframed)
|
62
|
+
def generate_order_url
|
63
63
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
64
|
+
# check if the config is empty, if yes, we try load what was set by the
|
65
|
+
# initializer into Pesapal.config
|
66
|
+
if config.empty?
|
67
|
+
set_configuration Pesapal.config[@mode]
|
68
|
+
end
|
68
69
|
|
69
|
-
|
70
|
-
|
71
|
-
|
70
|
+
# build xml with input data, the format is standard so no editing is
|
71
|
+
# required
|
72
|
+
@post_xml = Pesapal::Post::generate_post_xml @order_details
|
72
73
|
|
73
|
-
|
74
|
-
|
74
|
+
# initialize setting of @params (oauth_signature left empty)
|
75
|
+
@params = Pesapal::Post::set_parameters(@config[:callback_url], @config[:consumer_key], @post_xml)
|
75
76
|
|
76
|
-
|
77
|
-
|
77
|
+
# generate oauth signature and add signature to the request parameters
|
78
|
+
@params[:oauth_signature] = Pesapal::Oauth::generate_oauth_signature("GET", @api_endpoints[:postpesapaldirectorderv4], @params, @config[:consumer_secret], @token_secret)
|
78
79
|
|
79
|
-
|
80
|
-
|
80
|
+
# change params (with signature) to a query string
|
81
|
+
query_string = Pesapal::Oauth::generate_encoded_params_query_string @params
|
81
82
|
|
82
|
-
|
83
|
-
|
83
|
+
"#{@api_endpoints[:postpesapaldirectorderv4]}?#{query_string}"
|
84
|
+
end
|
84
85
|
|
85
|
-
|
86
|
-
|
86
|
+
# query the details of the transaction
|
87
|
+
def query_payment_details(merchant_reference, transaction_tracking_id)
|
87
88
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
# check if the config is empty, if yes, we try load what was set by the
|
90
|
+
# initializer into Pesapal.config
|
91
|
+
if config.empty?
|
92
|
+
set_configuration Pesapal.config[@mode]
|
93
|
+
end
|
92
94
|
|
93
|
-
|
94
|
-
|
95
|
+
# initialize setting of @params (oauth_signature left empty)
|
96
|
+
@params = Pesapal::Details::set_parameters(@config[:consumer_key], merchant_reference, transaction_tracking_id)
|
95
97
|
|
96
|
-
|
97
|
-
|
98
|
+
# generate oauth signature and add signature to the request parameters
|
99
|
+
@params[:oauth_signature] = Pesapal::Oauth::generate_oauth_signature("GET", @api_endpoints[:querypaymentdetails], @params, @config[:consumer_secret], @token_secret)
|
98
100
|
|
99
|
-
|
100
|
-
|
101
|
+
# change params (with signature) to a query string
|
102
|
+
query_string = Pesapal::Oauth::generate_encoded_params_query_string @params
|
101
103
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
104
|
+
# get status response
|
105
|
+
response = Net::HTTP.get(URI("#{@api_endpoints[:querypaymentdetails]}?#{query_string}"))
|
106
|
+
response = CGI::parse(response)
|
107
|
+
response = response["pesapal_response_data"][0].split(',')
|
106
108
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
109
|
+
details = { :method => response[1],
|
110
|
+
:status => response[2],
|
111
|
+
:merchant_reference => response[3],
|
112
|
+
:transaction_tracking_id => response[0] }
|
113
|
+
end
|
112
114
|
|
113
|
-
|
114
|
-
|
115
|
+
# query the status of the transaction
|
116
|
+
def query_payment_status(merchant_reference, transaction_tracking_id = nil)
|
115
117
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
118
|
+
# check if the config is empty, if yes, we try load what was set by the
|
119
|
+
# initializer into Pesapal.config
|
120
|
+
if config.empty?
|
121
|
+
set_configuration Pesapal.config[@mode]
|
122
|
+
end
|
120
123
|
|
121
|
-
|
122
|
-
|
124
|
+
# initialize setting of @params (oauth_signature left empty)
|
125
|
+
@params = Pesapal::Status::set_parameters(@config[:consumer_key], merchant_reference, transaction_tracking_id)
|
123
126
|
|
124
|
-
|
125
|
-
|
127
|
+
# generate oauth signature and add signature to the request parameters
|
128
|
+
@params[:oauth_signature] = Pesapal::Oauth::generate_oauth_signature("GET", @api_endpoints[:querypaymentstatus], @params, @config[:consumer_secret], @token_secret)
|
126
129
|
|
127
|
-
|
128
|
-
|
130
|
+
# change params (with signature) to a query string
|
131
|
+
query_string = Pesapal::Oauth::generate_encoded_params_query_string @params
|
129
132
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
+
# get status response
|
134
|
+
response = Net::HTTP.get(URI("#{@api_endpoints[:querypaymentstatus]}?#{query_string}"))
|
135
|
+
response = CGI::parse(response)
|
133
136
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
+
# return the string result of what we want
|
138
|
+
response["pesapal_response_data"][0]
|
139
|
+
end
|
137
140
|
|
138
|
-
|
139
|
-
|
141
|
+
# set mode when called
|
142
|
+
def set_mode(mode = :development)
|
140
143
|
|
141
|
-
|
142
|
-
|
144
|
+
# convert symbol to string and downcase
|
145
|
+
@mode = "#{mode.to_s.downcase}"
|
143
146
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
+
# set api endpoints depending on the mode
|
148
|
+
set_endpoints
|
149
|
+
end
|
147
150
|
|
148
|
-
|
149
|
-
|
151
|
+
# listen to ipn response
|
152
|
+
def ipn_listener(notification_type, merchant_reference, transaction_tracking_id)
|
150
153
|
|
151
|
-
|
154
|
+
status = query_payment_status(merchant_reference, transaction_tracking_id)
|
152
155
|
|
153
|
-
|
156
|
+
output = { :status => status }
|
154
157
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
158
|
+
if status == "COMPLETED"
|
159
|
+
output[:response] = "pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=#{transaction_tracking_id}&pesapal_merchant_reference=#{merchant_reference}"
|
160
|
+
elsif status == "FAILED"
|
161
|
+
output[:response] = "pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=#{transaction_tracking_id}&pesapal_merchant_reference=#{merchant_reference}"
|
162
|
+
else
|
163
|
+
output[:response] = ""
|
164
|
+
end
|
162
165
|
|
163
|
-
|
164
|
-
|
166
|
+
output
|
167
|
+
end
|
165
168
|
|
166
|
-
|
169
|
+
private
|
167
170
|
|
168
|
-
|
169
|
-
|
171
|
+
# set endpoints
|
172
|
+
def set_endpoints
|
170
173
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
174
|
+
if @mode == 'production'
|
175
|
+
@api_domain = 'https://www.pesapal.com'
|
176
|
+
else
|
177
|
+
@api_domain = 'http://demo.pesapal.com'
|
178
|
+
end
|
176
179
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
180
|
+
@api_endpoints = {}
|
181
|
+
@api_endpoints[:postpesapaldirectorderv4] = "#{@api_domain}/API/PostPesapalDirectOrderV4"
|
182
|
+
@api_endpoints[:querypaymentstatus] = "#{@api_domain}/API/QueryPaymentStatus"
|
183
|
+
@api_endpoints[:querypaymentdetails] = "#{@api_domain}/API/QueryPaymentDetails"
|
184
|
+
end
|
182
185
|
|
183
|
-
|
184
|
-
|
186
|
+
# set credentialts through hash, uses default if nothing is input
|
187
|
+
def set_configuration(consumer_details = {})
|
185
188
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
189
|
+
# set the configuration
|
190
|
+
@config = { :callback_url => 'http://0.0.0.0:3000/pesapal/callback',
|
191
|
+
:consumer_key => '<YOUR_CONSUMER_KEY>',
|
192
|
+
:consumer_secret => '<YOUR_CONSUMER_SECRET>'
|
193
|
+
}
|
191
194
|
|
192
|
-
|
195
|
+
valid_config_keys = @config.keys
|
193
196
|
|
194
|
-
|
195
|
-
|
197
|
+
consumer_details.each { |k,v| @config[k.to_sym] = v if valid_config_keys.include? k.to_sym }
|
198
|
+
end
|
196
199
|
|
197
|
-
|
198
|
-
|
200
|
+
# set configuration through yaml file
|
201
|
+
def set_configuration_from_yaml(path_to_file)
|
199
202
|
|
200
|
-
|
203
|
+
if File.exist?(path_to_file)
|
201
204
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
205
|
+
# load file, read it and parse the YAML
|
206
|
+
begin
|
207
|
+
loaded_config = YAML::load(IO.read(path_to_file))
|
208
|
+
rescue Errno::ENOENT
|
209
|
+
logger.info("YAML configuration file couldn't be found. Using defaults."); return
|
210
|
+
rescue Psych::SyntaxError
|
211
|
+
logger.info("YAML configuration file contains invalid syntax. Using defaults."); return
|
212
|
+
end
|
210
213
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
214
|
+
# pick the correct settings depending on the the mode and set it
|
215
|
+
# appropriately. this file is expected to have the settings for
|
216
|
+
# development and production
|
217
|
+
set_configuration loaded_config[@mode]
|
215
218
|
|
216
|
-
|
219
|
+
else
|
217
220
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
221
|
+
# in this case default values will be set
|
222
|
+
set_configuration
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
223
226
|
end
|
@@ -1,24 +1,24 @@
|
|
1
1
|
module Pesapal
|
2
2
|
|
3
|
-
|
3
|
+
module Details
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
# set parameters required by the QueryPaymentDetails call
|
6
|
+
def Details.set_parameters(consumer_key, merchant_reference, transaction_tracking_id)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
# parameters required by the QueryPaymentDetails call (excludes
|
9
|
+
# oauth_signature parameter as per the instructions here
|
10
|
+
# http://developer.pesapal.com/how-to-integrate/api-reference#QueryPaymentDetails)
|
11
11
|
|
12
|
-
|
12
|
+
timestamp = Time.now.to_i.to_s
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
14
|
+
params = { :oauth_consumer_key => consumer_key,
|
15
|
+
:oauth_nonce => "#{timestamp}" + Pesapal::Oauth.generate_nonce(12),
|
16
|
+
:oauth_signature_method => 'HMAC-SHA1',
|
17
|
+
:oauth_timestamp => "#{timestamp}",
|
18
|
+
:oauth_version => '1.0',
|
19
|
+
:pesapal_merchant_reference => merchant_reference,
|
20
|
+
:pesapal_transaction_tracking_id => transaction_tracking_id
|
21
|
+
}
|
23
22
|
end
|
23
|
+
end
|
24
24
|
end
|
@@ -1,51 +1,51 @@
|
|
1
1
|
module Pesapal
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
# set parameters required by the PostPesapalDirectOrderV4 call
|
33
|
-
def Post.set_parameters(callback_url, consumer_key, post_xml)
|
34
|
-
|
35
|
-
# parameters required by the PostPesapalDirectOrderV4 call (excludes
|
36
|
-
# oauth_signature parameter as per the instructions here
|
37
|
-
# http://developer.pesapal.com/how-to-integrate/api-reference#PostPesapalDirectOrderV4)
|
38
|
-
|
39
|
-
timestamp = Time.now.to_i.to_s
|
40
|
-
|
41
|
-
params = { :oauth_callback => callback_url,
|
42
|
-
:oauth_consumer_key => consumer_key,
|
43
|
-
:oauth_nonce => "#{timestamp}" + Pesapal::Oauth.generate_nonce(12),
|
44
|
-
:oauth_signature_method => 'HMAC-SHA1',
|
45
|
-
:oauth_timestamp => "#{timestamp}",
|
46
|
-
:oauth_version => '1.0',
|
47
|
-
:pesapal_request_data => post_xml
|
48
|
-
}
|
49
|
-
end
|
3
|
+
module Post
|
4
|
+
|
5
|
+
# build html encoded xml string for PostPesapalDirectOrderV4
|
6
|
+
def Post.generate_post_xml(details)
|
7
|
+
|
8
|
+
# build xml with input data, the format is standard so no editing is
|
9
|
+
# required
|
10
|
+
post_xml = ''
|
11
|
+
post_xml.concat '<?xml version="1.0" encoding="utf-8"?>'
|
12
|
+
post_xml.concat '<PesapalDirectOrderInfo '
|
13
|
+
post_xml.concat 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
14
|
+
post_xml.concat 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '
|
15
|
+
post_xml.concat "Amount=\"#{details[:amount]}\" "
|
16
|
+
post_xml.concat "Description=\"#{details[:description]}\" "
|
17
|
+
post_xml.concat "Type=\"#{details[:type]}\" "
|
18
|
+
post_xml.concat "Reference=\"#{details[:reference]}\" "
|
19
|
+
post_xml.concat "FirstName=\"#{details[:first_name]}\" "
|
20
|
+
post_xml.concat "LastName=\"#{details[:last_name]}\" "
|
21
|
+
post_xml.concat "Email=\"#{details[:email]}\" "
|
22
|
+
post_xml.concat "PhoneNumber=\"#{details[:phonenumber]}\" "
|
23
|
+
post_xml.concat "Currency=\"#{details[:currency]}\" "
|
24
|
+
post_xml.concat 'xmlns="http://www.pesapal.com" />'
|
25
|
+
|
26
|
+
encoder = HTMLEntities.new(:xhtml1)
|
27
|
+
post_xml = encoder.encode post_xml
|
28
|
+
|
29
|
+
"#{post_xml}"
|
50
30
|
end
|
51
|
-
|
31
|
+
|
32
|
+
# set parameters required by the PostPesapalDirectOrderV4 call
|
33
|
+
def Post.set_parameters(callback_url, consumer_key, post_xml)
|
34
|
+
|
35
|
+
# parameters required by the PostPesapalDirectOrderV4 call (excludes
|
36
|
+
# oauth_signature parameter as per the instructions here
|
37
|
+
# http://developer.pesapal.com/how-to-integrate/api-reference#PostPesapalDirectOrderV4)
|
38
|
+
|
39
|
+
timestamp = Time.now.to_i.to_s
|
40
|
+
|
41
|
+
params = { :oauth_callback => callback_url,
|
42
|
+
:oauth_consumer_key => consumer_key,
|
43
|
+
:oauth_nonce => "#{timestamp}" + Pesapal::Oauth.generate_nonce(12),
|
44
|
+
:oauth_signature_method => 'HMAC-SHA1',
|
45
|
+
:oauth_timestamp => "#{timestamp}",
|
46
|
+
:oauth_version => '1.0',
|
47
|
+
:pesapal_request_data => post_xml
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,29 +1,29 @@
|
|
1
1
|
module Pesapal
|
2
2
|
|
3
|
-
|
3
|
+
module Status
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
# set parameters required by the QueryPaymentStatus & QueryPaymentStatusByMerchantRef calls
|
6
|
+
def Status.set_parameters(consumer_key, merchant_reference, transaction_tracking_id = nil)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
# parameters required by the QueryPaymentStatus call (excludes
|
9
|
+
# oauth_signature parameter as per the instructions here
|
10
|
+
# http://developer.pesapal.com/how-to-integrate/api-reference)
|
11
11
|
|
12
|
-
|
12
|
+
timestamp = Time.now.to_i.to_s
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
params = { :oauth_consumer_key => consumer_key,
|
15
|
+
:oauth_nonce => "#{timestamp}" + Pesapal::Oauth.generate_nonce(12),
|
16
|
+
:oauth_signature_method => 'HMAC-SHA1',
|
17
|
+
:oauth_timestamp => "#{timestamp}",
|
18
|
+
:oauth_version => '1.0',
|
19
|
+
:pesapal_merchant_reference => merchant_reference
|
20
|
+
}
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
unless transaction_tracking_id.nil? # do, if not true
|
23
|
+
params[:pesapal_transaction_tracking_id] = transaction_tracking_id
|
24
|
+
end
|
25
25
|
|
26
|
-
|
27
|
-
end
|
26
|
+
params
|
28
27
|
end
|
28
|
+
end
|
29
29
|
end
|
data/lib/pesapal/oauth.rb
CHANGED
@@ -1,178 +1,178 @@
|
|
1
1
|
module Pesapal
|
2
2
|
|
3
|
-
|
3
|
+
module Oauth
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
# generate query string from parameters hash
|
6
|
+
def Oauth.generate_encoded_params_query_string(params = {})
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
# the oauth spec says to sort lexigraphically, which is the default
|
18
|
+
# alphabetical sort for many libraries. in case of two parameters with
|
19
|
+
# the same encoded key, the oauth spec says to continue sorting based on
|
20
|
+
# value
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
queries = []
|
23
|
+
params.each do |k,v| queries.push "#{self.parameter_encode(k.to_s)}=#{self.parameter_encode(v.to_s)}" end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
# parameters are sorted by name, using lexicographical byte value
|
26
|
+
# ordering
|
27
|
+
queries.sort!
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
queries.join('&')
|
30
|
+
end
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
# generate oauth nonce
|
33
|
+
def Oauth.generate_nonce(length)
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
# the consumer shall then generate a nonce value that is unique for all
|
36
|
+
# requests with that timestamp. a nonce is a random string, uniquely
|
37
|
+
# generated for each request. the nonce allows the service provider to
|
38
|
+
# verify that a request has never been made before and helps prevent
|
39
|
+
# replay attacks when requests are made over a non- secure channel (such
|
40
|
+
# as http).
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789'
|
43
|
+
nonce = ''
|
44
|
+
length.times { nonce << chars[rand(chars.size)] }
|
45
45
|
|
46
|
-
|
47
|
-
|
46
|
+
"#{nonce}"
|
47
|
+
end
|
48
48
|
|
49
|
-
|
50
|
-
|
49
|
+
# generate the oauth signature using hmac-sha1 algorithm
|
50
|
+
def Oauth.generate_oauth_signature(http_method, absolute_url, params, consumer_secret, token_secret = nil)
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
# the signature is calculated by passing the signature base string and
|
53
|
+
# signing key to the hmac-sha1 hashing algorithm. the output of the hmac
|
54
|
+
# signing function is a binary string. this needs to be base64 encoded to
|
55
|
+
# produce the signature string.
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
|
57
|
+
# for pesapal flow we don't have a token secret to we will set as nil and
|
58
|
+
# the appropriate action will be taken as per the oauth spec. see notes in
|
59
|
+
# the method that creates signing keys
|
60
60
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
# prepare the values we need
|
62
|
+
digest = OpenSSL::Digest::Digest.new('sha1')
|
63
|
+
signature_base_string = self.generate_signature_base_string(http_method, absolute_url, params)
|
64
|
+
signing_key = self.generate_signing_key(consumer_secret, token_secret)
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
66
|
+
hmac = OpenSSL::HMAC.digest(digest, signing_key, signature_base_string)
|
67
|
+
Base64.encode64(hmac).chomp
|
68
|
+
end
|
69
69
|
|
70
|
-
|
71
|
-
|
70
|
+
# generate query string from signable parameters hash
|
71
|
+
def Oauth.generate_signable_encoded_params_query_string(params = {})
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
# oauth_signature parameter MUST be excluded, assumes it was already
|
74
|
+
# initialized by calling set_parameters
|
75
|
+
params.delete(:oauth_signature)
|
76
76
|
|
77
|
-
|
78
|
-
|
77
|
+
self.generate_encoded_params_query_string params
|
78
|
+
end
|
79
79
|
|
80
|
-
|
81
|
-
|
80
|
+
# generate the oauth signature
|
81
|
+
def Oauth.generate_signature_base_string(http_method, absolute_url, params)
|
82
82
|
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
# three values collected so far must be joined to make a single string,
|
84
|
+
# from which the signature will be generated. This is called the
|
85
|
+
# signature base string by the OAuth specification
|
86
86
|
|
87
|
-
|
88
|
-
|
87
|
+
# step 1: convert the http method to uppercase
|
88
|
+
http_method = http_method.upcase
|
89
89
|
|
90
|
-
|
91
|
-
|
90
|
+
# step 2: percent encode the url
|
91
|
+
url_encoded = self.parameter_encode(self.normalized_request_uri(absolute_url))
|
92
92
|
|
93
|
-
|
94
|
-
|
93
|
+
# step 3: percent encode the parameter string
|
94
|
+
parameter_string_encoded = self.parameter_encode(self.generate_signable_encoded_params_query_string params)
|
95
95
|
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
99
99
|
|
100
|
-
|
101
|
-
|
100
|
+
"#{http_method}&#{url_encoded}&#{parameter_string_encoded}"
|
101
|
+
end
|
102
102
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
# the signing key is simply the percent encoded consumer secret,
|
107
|
-
# followed by an ampersand character '&', followed by the percent
|
108
|
-
# encoded token secret
|
109
|
-
|
110
|
-
# note that there are some flows, such as when obtaining a request
|
111
|
-
# token, where the token secret is not yet known. In this case, the
|
112
|
-
# signing key should consist of the percent encoded consumer secret
|
113
|
-
# followed by an ampersand character '&'
|
103
|
+
# generate signing key
|
104
|
+
def Oauth.generate_signing_key(consumer_secret, token_secret = nil)
|
114
105
|
|
115
|
-
|
116
|
-
|
106
|
+
# the signing key is simply the percent encoded consumer secret, followed
|
107
|
+
# by an ampersand character '&', followed by the percent encoded token
|
108
|
+
# secret
|
117
109
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
110
|
+
# note that there are some flows, such as when obtaining a request token,
|
111
|
+
# where the token secret is not yet known. In this case, the signing key
|
112
|
+
# should consist of the percent encoded consumer secret followed by an
|
113
|
+
# ampersand character '&'
|
122
114
|
|
123
|
-
|
124
|
-
|
115
|
+
# "#{@credentials[:consumer_secret]}"
|
116
|
+
consumer_secret_encoded = self.parameter_encode(consumer_secret)
|
125
117
|
|
126
|
-
|
127
|
-
|
118
|
+
token_secret_encoded = ""
|
119
|
+
unless token_secret.nil?
|
120
|
+
token_secret_encoded = self.parameter_encode(token_secret)
|
121
|
+
end
|
128
122
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
123
|
+
"#{consumer_secret_encoded}&#{token_secret_encoded}"
|
124
|
+
end
|
125
|
+
|
126
|
+
# normalize request absolute URL
|
127
|
+
def Oauth.normalized_request_uri(absolute_url)
|
134
128
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
# not available, the service provider should use the host name
|
140
|
-
# communicated to the consumer in the documentation or other means.
|
129
|
+
# the signature base string includes the request absolute url, tying the
|
130
|
+
# signature to a specific endpoint. the url used in the signature base
|
131
|
+
# string must include the scheme, authority, and path, and must exclude
|
132
|
+
# the query and fragment as defined by [rfc3986] section 3.
|
141
133
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
134
|
+
# if the absolute request url is not available to the service provider (it
|
135
|
+
# is always available to the consumer), it can be constructed by combining
|
136
|
+
# the scheme being used, the http host header, and the relative http
|
137
|
+
# request url. if the host header is not available, the service provider
|
138
|
+
# should use the host name communicated to the consumer in the
|
139
|
+
# documentation or other means.
|
147
140
|
|
148
|
-
|
141
|
+
# the service provider should document the form of url used in the
|
142
|
+
# signature base string to avoid ambiguity due to url normalization.
|
143
|
+
# unless specified, url scheme and authority must be lowercase and include
|
144
|
+
# the port number; http default port 80 and https default port 443 must be
|
145
|
+
# excluded.
|
149
146
|
|
150
|
-
|
151
|
-
host = u.host.downcase
|
152
|
-
path = u.path
|
153
|
-
port = u.port
|
147
|
+
u = URI.parse(absolute_url)
|
154
148
|
|
155
|
-
|
156
|
-
|
149
|
+
scheme = u.scheme.downcase
|
150
|
+
host = u.host.downcase
|
151
|
+
path = u.path
|
152
|
+
port = u.port
|
157
153
|
|
158
|
-
|
159
|
-
|
154
|
+
port = (scheme == 'http' && port != 80) || (scheme == 'https' && port != 443) ? ":#{port}" : ""
|
155
|
+
path = (path && path != '') ? path : '/'
|
156
|
+
|
157
|
+
"#{scheme}://#{host}#{port}#{path}"
|
158
|
+
end
|
160
159
|
|
161
|
-
|
162
|
-
|
160
|
+
# percentage encode value as per the oauth spec
|
161
|
+
def Oauth.parameter_encode(string)
|
163
162
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
# [rfc3629].
|
163
|
+
# all parameter names and values are escaped using the [rfc3986] percent-
|
164
|
+
# encoding (%xx) mechanism. characters not in the unreserved character set
|
165
|
+
# ([rfc3986] section 2.3) must be encoded. characters in the unreserved
|
166
|
+
# character set must not be encoded. hexadecimal characters in encodings
|
167
|
+
# must be upper case. text names and values must be encoded as utf-8
|
168
|
+
# octets before percent-encoding them per [rfc3629].
|
171
169
|
|
172
|
-
|
173
|
-
|
170
|
+
# reserved character regexp, per section 5.1
|
171
|
+
reserved_characters = /[^a-zA-Z0-9\-\.\_\~]/
|
174
172
|
|
175
|
-
|
176
|
-
|
173
|
+
# Apparently we can't force_encoding on a frozen string since that would modify it.
|
174
|
+
# What we can do is work with a copy
|
175
|
+
URI::escape(string.dup.to_s.force_encoding(Encoding::UTF_8), reserved_characters)
|
177
176
|
end
|
178
|
-
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/pesapal/version.rb
CHANGED
data/pesapal.gemspec
CHANGED
@@ -3,23 +3,23 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
3
|
require "pesapal/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
6
|
+
spec.name = "pesapal"
|
7
|
+
spec.version = Pesapal::VERSION
|
8
|
+
spec.date = Time.new.getlocal("+03:00").strftime("%Y-%m-%d")
|
9
|
+
spec.authors = ["Job King'ori Maina"]
|
10
|
+
spec.email = ["j@kingori.co"]
|
11
|
+
spec.description = "Make authenticated Pesapal API calls without the fuss!"
|
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://rubydoc.info/gems/pesapal/"
|
14
|
+
spec.license = "MIT"
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
23
|
|
24
|
-
|
25
|
-
end
|
24
|
+
spec.add_dependency "htmlentities"
|
25
|
+
end
|
metadata
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pesapal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.2
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
7
|
authors:
|
7
8
|
- Job King'ori Maina
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2013-
|
12
|
+
date: 2013-12-20 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: bundler
|
15
16
|
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
16
18
|
requirements:
|
17
19
|
- - ~>
|
18
20
|
- !ruby/object:Gem::Version
|
@@ -20,6 +22,7 @@ dependencies:
|
|
20
22
|
type: :development
|
21
23
|
prerelease: false
|
22
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
23
26
|
requirements:
|
24
27
|
- - ~>
|
25
28
|
- !ruby/object:Gem::Version
|
@@ -27,29 +30,33 @@ dependencies:
|
|
27
30
|
- !ruby/object:Gem::Dependency
|
28
31
|
name: rake
|
29
32
|
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
30
34
|
requirements:
|
31
|
-
- - '>='
|
35
|
+
- - ! '>='
|
32
36
|
- !ruby/object:Gem::Version
|
33
37
|
version: '0'
|
34
38
|
type: :development
|
35
39
|
prerelease: false
|
36
40
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
37
42
|
requirements:
|
38
|
-
- - '>='
|
43
|
+
- - ! '>='
|
39
44
|
- !ruby/object:Gem::Version
|
40
45
|
version: '0'
|
41
46
|
- !ruby/object:Gem::Dependency
|
42
47
|
name: htmlentities
|
43
48
|
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
44
50
|
requirements:
|
45
|
-
- - '>='
|
51
|
+
- - ! '>='
|
46
52
|
- !ruby/object:Gem::Version
|
47
53
|
version: '0'
|
48
54
|
type: :runtime
|
49
55
|
prerelease: false
|
50
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
51
58
|
requirements:
|
52
|
-
- - '>='
|
59
|
+
- - ! '>='
|
53
60
|
- !ruby/object:Gem::Version
|
54
61
|
version: '0'
|
55
62
|
description: Make authenticated Pesapal API calls without the fuss!
|
@@ -81,26 +88,27 @@ files:
|
|
81
88
|
homepage: http://rubydoc.info/gems/pesapal/
|
82
89
|
licenses:
|
83
90
|
- MIT
|
84
|
-
metadata: {}
|
85
91
|
post_install_message:
|
86
92
|
rdoc_options: []
|
87
93
|
require_paths:
|
88
94
|
- lib
|
89
95
|
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
90
97
|
requirements:
|
91
|
-
- - '>='
|
98
|
+
- - ! '>='
|
92
99
|
- !ruby/object:Gem::Version
|
93
100
|
version: '0'
|
94
101
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
95
103
|
requirements:
|
96
|
-
- - '>='
|
104
|
+
- - ! '>='
|
97
105
|
- !ruby/object:Gem::Version
|
98
106
|
version: '0'
|
99
107
|
requirements: []
|
100
108
|
rubyforge_project:
|
101
|
-
rubygems_version:
|
109
|
+
rubygems_version: 1.8.23
|
102
110
|
signing_key:
|
103
|
-
specification_version:
|
111
|
+
specification_version: 3
|
104
112
|
summary: Make authenticated Pesapal API calls without the fuss! Handles all the oAuth
|
105
113
|
stuff abstracting any direct interaction with the API endpoints so that you can
|
106
114
|
focus on what matters. Building awesome.
|
checksums.yaml
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz: 1b3d98d9279b31f66a9996b6045d19a0ae01c2c7
|
4
|
-
data.tar.gz: e132dc33bd5a88e3c2980e4c5eff9f8bfe438e33
|
5
|
-
SHA512:
|
6
|
-
metadata.gz: 64ff6e09c9b02066fe9b78120cc2f60594eea9fc3d23c5c010b2eb9eccce2ef3d5bd0d4d4de20b360f380a171278a18794e472dec65ff69f572e721a1797d61e
|
7
|
-
data.tar.gz: 510c5cadf3d39ed31c86d266c5023c9b79bd087329818401e801a2e3a1075f9bb858f7fd891f89966005b4ca3ce20a44d7571be948069dac60fd4640dc5f0fbb
|