activemerchant 1.35.1 → 1.36.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+
1
3
  module ActiveMerchant #:nodoc:
2
4
  module Billing #:nodoc:
3
5
  class StripeGateway < Gateway
@@ -30,15 +32,15 @@ module ActiveMerchant #:nodoc:
30
32
  def initialize(options = {})
31
33
  requires!(options, :login)
32
34
  @api_key = options[:login]
35
+ @fee_refund_api_key = options[:fee_refund_login]
33
36
  super
34
37
  end
35
38
 
36
39
  def authorize(money, creditcard, options = {})
37
40
  post = create_post_for_auth_or_purchase(money, creditcard, options)
38
41
  post[:capture] = "false"
39
- meta = generate_meta(options)
40
42
 
41
- commit(:post, 'charges', post, meta)
43
+ commit(:post, 'charges', post, generate_meta(options))
42
44
  end
43
45
 
44
46
  # To create a charge on a card or a token, call
@@ -50,14 +52,12 @@ module ActiveMerchant #:nodoc:
50
52
  # purchase(money, nil, { :customer => id, ... })
51
53
  def purchase(money, creditcard, options = {})
52
54
  post = create_post_for_auth_or_purchase(money, creditcard, options)
53
- meta = generate_meta(options)
54
55
 
55
- commit(:post, 'charges', post, meta)
56
+ commit(:post, 'charges', post, generate_meta(options))
56
57
  end
57
58
 
58
59
  def capture(money, authorization, options = {})
59
- post = {}
60
- post[:amount] = amount(money)
60
+ post = {:amount => amount(money)}
61
61
  add_application_fee(post, options)
62
62
 
63
63
  commit(:post, "charges/#{CGI.escape(authorization)}/capture", post)
@@ -68,13 +68,33 @@ module ActiveMerchant #:nodoc:
68
68
  end
69
69
 
70
70
  def refund(money, identification, options = {})
71
- meta = generate_meta(options)
72
- post = {}
71
+ post = {:amount => amount(money)}
72
+ commit_options = generate_meta(options)
73
+
74
+ MultiResponse.run do |r|
75
+ r.process { commit(:post, "charges/#{CGI.escape(identification)}/refund", post, commit_options) }
76
+
77
+ return r unless options[:refund_fee_amount]
78
+
79
+ r.process { fetch_application_fees(identification, commit_options) }
80
+ r.process { refund_application_fee(options[:refund_fee_amount], application_fee_from_response(r), commit_options) }
81
+ end
82
+ end
73
83
 
74
- post[:amount] = amount(money) if money
75
- post[:refund_application_fee] = true if options[:refund_application_fee]
84
+ def application_fee_from_response(response)
85
+ return unless response.success?
76
86
 
77
- commit(:post, "charges/#{CGI.escape(identification)}/refund", post, meta)
87
+ application_fees = response.params["data"].select { |fee| fee["object"] == "application_fee" }
88
+ application_fees.first["id"] unless application_fees.empty?
89
+ end
90
+
91
+ def refund_application_fee(money, identification, options = {})
92
+ return Response.new(false, "Application fee id could not be found") unless identification
93
+
94
+ post = {:amount => amount(money)}
95
+ options.merge!(:key => @fee_refund_api_key)
96
+
97
+ commit(:post, "application_fees/#{CGI.escape(identification)}/refund", post, options)
78
98
  end
79
99
 
80
100
  def store(creditcard, options = {})
@@ -83,14 +103,13 @@ module ActiveMerchant #:nodoc:
83
103
  post[:description] = options[:description]
84
104
  post[:email] = options[:email]
85
105
 
86
- meta = generate_meta(options)
87
106
  path = if options[:customer]
88
107
  "customers/#{CGI.escape(options[:customer])}"
89
108
  else
90
109
  'customers'
91
110
  end
92
111
 
93
- commit(:post, path, post, meta)
112
+ commit(:post, path, post, generate_meta(options))
94
113
  end
95
114
 
96
115
  def update(customer_id, creditcard, options = {})
@@ -99,8 +118,7 @@ module ActiveMerchant #:nodoc:
99
118
  end
100
119
 
101
120
  def unstore(customer_id, options = {})
102
- meta = generate_meta(options)
103
- commit(:delete, "customers/#{CGI.escape(customer_id)}", nil, meta)
121
+ commit(:delete, "customers/#{CGI.escape(customer_id)}", nil, generate_meta(options))
104
122
  end
105
123
 
106
124
  private
@@ -179,6 +197,12 @@ module ActiveMerchant #:nodoc:
179
197
  post[:uncaptured] = true if options[:uncaptured]
180
198
  end
181
199
 
200
+ def fetch_application_fees(identification, options = {})
201
+ options.merge!(:key => @fee_refund_api_key)
202
+
203
+ commit(:get, "application_fees?charge=#{identification}", nil, options)
204
+ end
205
+
182
206
  def parse(body)
183
207
  JSON.parse(body)
184
208
  end
@@ -201,10 +225,10 @@ module ActiveMerchant #:nodoc:
201
225
  end
202
226
 
203
227
  def generate_meta(options)
204
- {:ip => options[:ip]}
228
+ {:meta => {:ip => options[:ip]}}
205
229
  end
206
230
 
207
- def headers(meta={})
231
+ def headers(options = {})
208
232
  @@ua ||= JSON.dump({
209
233
  :bindings_version => ActiveMerchant::VERSION,
210
234
  :lang => 'ruby',
@@ -214,19 +238,21 @@ module ActiveMerchant #:nodoc:
214
238
  :uname => (RUBY_PLATFORM =~ /linux|darwin/i ? `uname -a 2>/dev/null`.strip : nil)
215
239
  })
216
240
 
241
+ key = options[:key] || @api_key
242
+
217
243
  {
218
- "Authorization" => "Basic " + Base64.encode64(@api_key.to_s + ":").strip,
244
+ "Authorization" => "Basic " + Base64.encode64(key.to_s + ":").strip,
219
245
  "User-Agent" => "Stripe/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}",
220
246
  "X-Stripe-Client-User-Agent" => @@ua,
221
- "X-Stripe-Client-User-Metadata" => meta.to_json
247
+ "X-Stripe-Client-User-Metadata" => options[:meta].to_json
222
248
  }
223
249
  end
224
250
 
225
- def commit(method, url, parameters=nil, meta={})
251
+ def commit(method, url, parameters=nil, options = {})
226
252
  raw_response = response = nil
227
253
  success = false
228
254
  begin
229
- raw_response = ssl_request(method, self.live_url + url, post_data(parameters), headers(meta))
255
+ raw_response = ssl_request(method, self.live_url + url, post_data(parameters), headers(options))
230
256
  response = parse(raw_response)
231
257
  success = !response.key?("error")
232
258
  rescue ResponseError => e
@@ -21,6 +21,10 @@ module ActiveMerchant #:nodoc:
21
21
  raise NotImplementedError.new
22
22
  end
23
23
 
24
+ def refund_fee(identification, options, meta)
25
+ raise NotImplementedError.new
26
+ end
27
+
24
28
  def json_error(raw_response)
25
29
  msg = 'Invalid response received from the WebPay API. Please contact support@webpay.jp if you continue to receive this message.'
26
30
  msg += " (The raw response returned by the API was #{raw_response.inspect})"
@@ -31,7 +35,7 @@ module ActiveMerchant #:nodoc:
31
35
  }
32
36
  end
33
37
 
34
- def headers(meta={})
38
+ def headers(options = {})
35
39
  @@ua ||= JSON.dump({
36
40
  :bindings_version => ActiveMerchant::VERSION,
37
41
  :lang => 'ruby',
@@ -45,7 +49,7 @@ module ActiveMerchant #:nodoc:
45
49
  "Authorization" => "Basic " + Base64.encode64(@api_key.to_s + ":").strip,
46
50
  "User-Agent" => "Webpay/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}",
47
51
  "X-Webpay-Client-User-Agent" => @@ua,
48
- "X-Webpay-Client-User-Metadata" => meta.to_json
52
+ "X-Webpay-Client-User-Metadata" => options[:meta].to_json
49
53
  }
50
54
  end
51
55
  end
@@ -0,0 +1,28 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ module Integrations #:nodoc:
4
+ # Platron API: www.platron.ru/PlatronAPI.pdf‎
5
+ module Platron
6
+ autoload :Helper, File.dirname(__FILE__) + '/platron/helper.rb'
7
+ autoload :Notification, File.dirname(__FILE__) + '/platron/notification.rb'
8
+ autoload :Common, File.dirname(__FILE__) + '/platron/common.rb'
9
+
10
+ mattr_accessor :service_url
11
+ self.service_url = 'https://www.platron.ru/payment.php'
12
+
13
+ def self.notification(raw_post)
14
+ Notification.new(raw_post)
15
+ end
16
+
17
+ def self.generate_signature_string(params, path, secret)
18
+ sorted_params = params.sort_by{|k,v| k.to_s}.collect{|k,v| v}
19
+ [path, sorted_params, secret].flatten.compact.join(';')
20
+ end
21
+
22
+ def self.generate_signature(params, path, secret)
23
+ Digest::MD5.hexdigest(generate_signature_string(params, path, secret))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ module Integrations #:nodoc:
4
+ module Platron
5
+ class Helper < ActiveMerchant::Billing::Integrations::Helper
6
+ def initialize(order, account, options = {})
7
+ @secret_key = options.delete(:secret)
8
+ @path = options.delete(:path)
9
+ description = options.delete(:description)
10
+ super
11
+ self.add_field('pg_salt', rand(36**15).to_s(36))
12
+ self.add_field('pg_description', description)
13
+ end
14
+
15
+ def form_fields
16
+ @fields.merge('pg_sig' => Common.generate_signature(@fields, @path, @secret_key))
17
+ end
18
+
19
+ def params
20
+ @fields
21
+ end
22
+
23
+ mapping :account, 'pg_merchant_id'
24
+ mapping :amount, 'pg_amount'
25
+ mapping :order, 'pg_order_id'
26
+ mapping :description, 'pg_description'
27
+ mapping :currency, 'pg_currency'
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,113 @@
1
+ require 'builder'
2
+
3
+ module ActiveMerchant #:nodoc:
4
+ module Billing #:nodoc:
5
+ module Integrations #:nodoc:
6
+ module Platron
7
+ class Notification < ActiveMerchant::Billing::Integrations::Notification
8
+ def initialize(*args)
9
+ super
10
+ @signature = params.delete('pg_sig')
11
+ end
12
+
13
+ def complete?
14
+ params['pg_result']
15
+ end
16
+
17
+ def order_id
18
+ params['pg_order_id']
19
+ end
20
+
21
+ def platron_payment_id
22
+ params['pg_payment_id']
23
+ end
24
+
25
+ def currency
26
+ params['pg_ps_currency']
27
+ end
28
+
29
+ def payment_system
30
+ params['pg_payment_system']
31
+ end
32
+
33
+ def user_phone
34
+ params['pg_user_phone']
35
+ end
36
+
37
+ def card_brand
38
+ params['pg_card_brand']
39
+ end
40
+
41
+ def captured
42
+ params['pg_captured']
43
+ end
44
+
45
+ def overpayment
46
+ params['pg_overpayment']
47
+ end
48
+
49
+ def failure_code
50
+ params['pg_failure_code']
51
+ end
52
+
53
+ def failure_description
54
+ params['pg_failure_description']
55
+ end
56
+
57
+ def payment_date
58
+ params['pg_payment_date']
59
+ end
60
+
61
+ def salt
62
+ params['pg_salt']
63
+ end
64
+
65
+ def signature
66
+ @signature
67
+ end
68
+
69
+ def net_amount
70
+ params['pg_net_amount']
71
+ end
72
+
73
+ def ps_amount
74
+ params['pg_ps_amount']
75
+ end
76
+
77
+ def ps_full_amount
78
+ params['pg_ps_full_amount']
79
+ end
80
+
81
+ def amount
82
+ params['pg_amount']
83
+ end
84
+
85
+ def secret
86
+ @options[:secret]
87
+ end
88
+
89
+ def path
90
+ @options[:path]
91
+ end
92
+
93
+ def acknowledge
94
+ signature == Platron.generate_signature(params, path, secret)
95
+ end
96
+
97
+ def success_response(path,secret)
98
+ salt = rand(36**15).to_s(36)
99
+ xml = ""
100
+ doc = Builder::XmlMarkup.new(:target => xml)
101
+ sign = Platron.generate_signature({:pg_status => 'ok', :pg_salt => salt}, path, secret)
102
+ doc.response do
103
+ doc.pg_status 'ok'
104
+ doc.pg_salt salt
105
+ doc.pg_sig sign
106
+ end
107
+ xml
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -32,6 +32,7 @@ module ActiveMerchant #:nodoc:
32
32
  mapping :fail_url, 'LMI_FAIL_URL'
33
33
  mapping :success_url, 'LMI_SUCCESS_URL'
34
34
  mapping :result_url, 'LMI_RESULT_URL'
35
+ mapping :debug, 'LMI_SIM_MODE'
35
36
  end
36
37
  end
37
38
  end
@@ -36,6 +36,10 @@ module ActiveMerchant #:nodoc:
36
36
  def acknowledge
37
37
  (security_key == generate_signature)
38
38
  end
39
+
40
+ def success_response(*args)
41
+ {:nothing => true}
42
+ end
39
43
  end
40
44
  end
41
45
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveMerchant
2
- VERSION = "1.35.1"
2
+ VERSION = "1.36.0"
3
3
  end
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activemerchant
3
3
  version: !ruby/object:Gem::Version
4
+ version: 1.36.0
4
5
  prerelease:
5
- version: 1.35.1
6
6
  platform: ruby
7
7
  authors:
8
8
  - Tobias Luetke
@@ -37,184 +37,184 @@ cert_chain:
37
37
  Z1BvU1BxN25rK3MyRlFVQko5VVpGSzFsZ016aG8vNGZaZ3pKd2J1K2NPOFNO
38
38
  dWFMUy9iagpoUGFTVHlWVTB5Q1Nudz09Ci0tLS0tRU5EIENFUlRJRklDQVRF
39
39
  LS0tLS0K
40
- date: 2013-07-22 00:00:00.000000000 Z
40
+ date: 2013-08-02 00:00:00.000000000 Z
41
41
  dependencies:
42
42
  - !ruby/object:Gem::Dependency
43
- version_requirements: !ruby/object:Gem::Requirement
43
+ name: activesupport
44
+ requirement: !ruby/object:Gem::Requirement
45
+ none: false
44
46
  requirements:
45
47
  - - ! '>='
46
48
  - !ruby/object:Gem::Version
47
49
  version: 2.3.14
48
- none: false
49
- name: activesupport
50
50
  type: :runtime
51
51
  prerelease: false
52
- requirement: !ruby/object:Gem::Requirement
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ none: false
53
54
  requirements:
54
55
  - - ! '>='
55
56
  - !ruby/object:Gem::Version
56
57
  version: 2.3.14
57
- none: false
58
58
  - !ruby/object:Gem::Dependency
59
- version_requirements: !ruby/object:Gem::Requirement
59
+ name: i18n
60
+ requirement: !ruby/object:Gem::Requirement
61
+ none: false
60
62
  requirements:
61
63
  - - ! '>='
62
64
  - !ruby/object:Gem::Version
63
65
  version: '0'
64
- none: false
65
- name: i18n
66
66
  type: :runtime
67
67
  prerelease: false
68
- requirement: !ruby/object:Gem::Requirement
68
+ version_requirements: !ruby/object:Gem::Requirement
69
+ none: false
69
70
  requirements:
70
71
  - - ! '>='
71
72
  - !ruby/object:Gem::Version
72
73
  version: '0'
73
- none: false
74
74
  - !ruby/object:Gem::Dependency
75
- version_requirements: !ruby/object:Gem::Requirement
75
+ name: money
76
+ requirement: !ruby/object:Gem::Requirement
77
+ none: false
76
78
  requirements:
77
79
  - - ! '>='
78
80
  - !ruby/object:Gem::Version
79
81
  version: '0'
80
- none: false
81
- name: money
82
82
  type: :runtime
83
83
  prerelease: false
84
- requirement: !ruby/object:Gem::Requirement
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ none: false
85
86
  requirements:
86
87
  - - ! '>='
87
88
  - !ruby/object:Gem::Version
88
89
  version: '0'
89
- none: false
90
90
  - !ruby/object:Gem::Dependency
91
- version_requirements: !ruby/object:Gem::Requirement
91
+ name: builder
92
+ requirement: !ruby/object:Gem::Requirement
93
+ none: false
92
94
  requirements:
93
95
  - - ! '>='
94
96
  - !ruby/object:Gem::Version
95
97
  version: 2.0.0
96
- none: false
97
- name: builder
98
98
  type: :runtime
99
99
  prerelease: false
100
- requirement: !ruby/object:Gem::Requirement
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ none: false
101
102
  requirements:
102
103
  - - ! '>='
103
104
  - !ruby/object:Gem::Version
104
105
  version: 2.0.0
105
- none: false
106
106
  - !ruby/object:Gem::Dependency
107
- version_requirements: !ruby/object:Gem::Requirement
107
+ name: json
108
+ requirement: !ruby/object:Gem::Requirement
109
+ none: false
108
110
  requirements:
109
111
  - - ! '>='
110
112
  - !ruby/object:Gem::Version
111
113
  version: 1.5.1
112
- none: false
113
- name: json
114
114
  type: :runtime
115
115
  prerelease: false
116
- requirement: !ruby/object:Gem::Requirement
116
+ version_requirements: !ruby/object:Gem::Requirement
117
+ none: false
117
118
  requirements:
118
119
  - - ! '>='
119
120
  - !ruby/object:Gem::Version
120
121
  version: 1.5.1
121
- none: false
122
122
  - !ruby/object:Gem::Dependency
123
- version_requirements: !ruby/object:Gem::Requirement
123
+ name: active_utils
124
+ requirement: !ruby/object:Gem::Requirement
125
+ none: false
124
126
  requirements:
125
127
  - - ! '>='
126
128
  - !ruby/object:Gem::Version
127
129
  version: 1.0.2
128
- none: false
129
- name: active_utils
130
130
  type: :runtime
131
131
  prerelease: false
132
- requirement: !ruby/object:Gem::Requirement
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ none: false
133
134
  requirements:
134
135
  - - ! '>='
135
136
  - !ruby/object:Gem::Version
136
137
  version: 1.0.2
137
- none: false
138
138
  - !ruby/object:Gem::Dependency
139
- version_requirements: !ruby/object:Gem::Requirement
139
+ name: nokogiri
140
+ requirement: !ruby/object:Gem::Requirement
141
+ none: false
140
142
  requirements:
141
143
  - - <
142
144
  - !ruby/object:Gem::Version
143
145
  version: 1.6.0
144
- none: false
145
- name: nokogiri
146
146
  type: :runtime
147
147
  prerelease: false
148
- requirement: !ruby/object:Gem::Requirement
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ none: false
149
150
  requirements:
150
151
  - - <
151
152
  - !ruby/object:Gem::Version
152
153
  version: 1.6.0
153
- none: false
154
154
  - !ruby/object:Gem::Dependency
155
- version_requirements: !ruby/object:Gem::Requirement
155
+ name: rake
156
+ requirement: !ruby/object:Gem::Requirement
157
+ none: false
156
158
  requirements:
157
159
  - - ! '>='
158
160
  - !ruby/object:Gem::Version
159
161
  version: '0'
160
- none: false
161
- name: rake
162
162
  type: :development
163
163
  prerelease: false
164
- requirement: !ruby/object:Gem::Requirement
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ none: false
165
166
  requirements:
166
167
  - - ! '>='
167
168
  - !ruby/object:Gem::Version
168
169
  version: '0'
169
- none: false
170
170
  - !ruby/object:Gem::Dependency
171
- version_requirements: !ruby/object:Gem::Requirement
171
+ name: mocha
172
+ requirement: !ruby/object:Gem::Requirement
173
+ none: false
172
174
  requirements:
173
175
  - - ~>
174
176
  - !ruby/object:Gem::Version
175
177
  version: 0.13.0
176
- none: false
177
- name: mocha
178
178
  type: :development
179
179
  prerelease: false
180
- requirement: !ruby/object:Gem::Requirement
180
+ version_requirements: !ruby/object:Gem::Requirement
181
+ none: false
181
182
  requirements:
182
183
  - - ~>
183
184
  - !ruby/object:Gem::Version
184
185
  version: 0.13.0
185
- none: false
186
186
  - !ruby/object:Gem::Dependency
187
- version_requirements: !ruby/object:Gem::Requirement
187
+ name: rails
188
+ requirement: !ruby/object:Gem::Requirement
189
+ none: false
188
190
  requirements:
189
191
  - - ! '>='
190
192
  - !ruby/object:Gem::Version
191
193
  version: 2.3.14
192
- none: false
193
- name: rails
194
194
  type: :development
195
195
  prerelease: false
196
- requirement: !ruby/object:Gem::Requirement
196
+ version_requirements: !ruby/object:Gem::Requirement
197
+ none: false
197
198
  requirements:
198
199
  - - ! '>='
199
200
  - !ruby/object:Gem::Version
200
201
  version: 2.3.14
201
- none: false
202
202
  - !ruby/object:Gem::Dependency
203
- version_requirements: !ruby/object:Gem::Requirement
203
+ name: thor
204
+ requirement: !ruby/object:Gem::Requirement
205
+ none: false
204
206
  requirements:
205
207
  - - ! '>='
206
208
  - !ruby/object:Gem::Version
207
209
  version: '0'
208
- none: false
209
- name: thor
210
210
  type: :development
211
211
  prerelease: false
212
- requirement: !ruby/object:Gem::Requirement
212
+ version_requirements: !ruby/object:Gem::Requirement
213
+ none: false
213
214
  requirements:
214
215
  - - ! '>='
215
216
  - !ruby/object:Gem::Version
216
217
  version: '0'
217
- none: false
218
218
  description: Active Merchant is a simple payment abstraction library used in and sponsored
219
219
  by Shopify. It is written by Tobias Luetke, Cody Fauser, and contributors. The aim
220
220
  of the project is to feel natural to Ruby users and to abstract as many parts as
@@ -469,6 +469,9 @@ files:
469
469
  - lib/active_merchant/billing/integrations/payu_in/notification.rb
470
470
  - lib/active_merchant/billing/integrations/payu_in/return.rb
471
471
  - lib/active_merchant/billing/integrations/payu_in.rb
472
+ - lib/active_merchant/billing/integrations/platron/helper.rb
473
+ - lib/active_merchant/billing/integrations/platron/notification.rb
474
+ - lib/active_merchant/billing/integrations/platron.rb
472
475
  - lib/active_merchant/billing/integrations/pxpay/helper.rb
473
476
  - lib/active_merchant/billing/integrations/pxpay/notification.rb
474
477
  - lib/active_merchant/billing/integrations/pxpay/return.rb
@@ -529,17 +532,17 @@ rdoc_options: []
529
532
  require_paths:
530
533
  - lib
531
534
  required_ruby_version: !ruby/object:Gem::Requirement
535
+ none: false
532
536
  requirements:
533
537
  - - ! '>='
534
538
  - !ruby/object:Gem::Version
535
539
  version: '0'
536
- none: false
537
540
  required_rubygems_version: !ruby/object:Gem::Requirement
541
+ none: false
538
542
  requirements:
539
543
  - - ! '>='
540
544
  - !ruby/object:Gem::Version
541
545
  version: '0'
542
- none: false
543
546
  requirements: []
544
547
  rubyforge_project: activemerchant
545
548
  rubygems_version: 1.8.23