codehog-google-checkout 1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +25 -0
- data/Isolate +13 -0
- data/MIT-LICENSE.txt +23 -0
- data/Manifest.txt +42 -0
- data/README.txt +148 -0
- data/Rakefile +35 -0
- data/examples/google_notifications_controller.rb +159 -0
- data/lib/duck_punches/hpricot.rb +24 -0
- data/lib/google-checkout.rb +61 -0
- data/lib/google-checkout/cart.rb +357 -0
- data/lib/google-checkout/command.rb +191 -0
- data/lib/google-checkout/notification.rb +226 -0
- data/spec/fixtures/google/checkout-shopping-cart.xml +22 -0
- data/spec/fixtures/google/commands/add-merchant-order-number.xml +5 -0
- data/spec/fixtures/google/commands/add-tracking-data.xml +8 -0
- data/spec/fixtures/google/commands/archive-order.xml +3 -0
- data/spec/fixtures/google/commands/authorize-order.xml +2 -0
- data/spec/fixtures/google/commands/cancel-order.xml +5 -0
- data/spec/fixtures/google/commands/charge-order.xml +4 -0
- data/spec/fixtures/google/commands/deliver-order.xml +9 -0
- data/spec/fixtures/google/commands/process-order.xml +2 -0
- data/spec/fixtures/google/commands/refund-order.xml +6 -0
- data/spec/fixtures/google/commands/send-buyer-message.xml +7 -0
- data/spec/fixtures/google/commands/unarchive-order.xml +2 -0
- data/spec/fixtures/google/notifications/authorization-amount-notification.xml +10 -0
- data/spec/fixtures/google/notifications/charge-amount-notification.xml +8 -0
- data/spec/fixtures/google/notifications/chargeback-amount-notification.xml +8 -0
- data/spec/fixtures/google/notifications/new-order-notification.xml +85 -0
- data/spec/fixtures/google/notifications/order-state-change-notification.xml +11 -0
- data/spec/fixtures/google/notifications/refund-amount-notification.xml +8 -0
- data/spec/fixtures/google/notifications/risk-information-notification.xml +23 -0
- data/spec/fixtures/google/responses/checkout-redirect.xml +5 -0
- data/spec/fixtures/google/responses/error.xml +5 -0
- data/spec/fixtures/google/responses/request-received.xml +3 -0
- data/spec/google-checkout/cart_spec.rb +165 -0
- data/spec/google-checkout/command_spec.rb +131 -0
- data/spec/google-checkout/notification_spec.rb +181 -0
- data/spec/google-checkout/response_spec.rb +49 -0
- data/spec/google-checkout_spec.rb +15 -0
- data/spec/spec_helper.rb +47 -0
- data/support/cacert.pem +7815 -0
- metadata +203 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
class Nokogiri::XML::NodeSet
|
3
|
+
|
4
|
+
##
|
5
|
+
# Assume a Google standard money node with a currency attribute.
|
6
|
+
#
|
7
|
+
# Returns a Ruby Money object.
|
8
|
+
|
9
|
+
def to_money
|
10
|
+
dollar_amount = inner_html
|
11
|
+
cents = (dollar_amount.to_f * 100).round
|
12
|
+
currency = first[:currency]
|
13
|
+
Money.new(cents, currency)
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Return boolean true if the value of an element is the
|
18
|
+
# string 'true'.
|
19
|
+
|
20
|
+
def to_boolean
|
21
|
+
inner_html == 'true'
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
##
|
2
|
+
# Ref:
|
3
|
+
#
|
4
|
+
# https://sandbox.google.com/checkout/cws/v2/Merchant/MERCHANT_ID/merchantCheckout
|
5
|
+
# https://checkout.google.com/cws/v2/Merchant/MERCHANT_ID/merchantCheckout
|
6
|
+
|
7
|
+
$: << File.dirname(__FILE__)
|
8
|
+
$: << File.dirname(__FILE__) + "/vendor/ruby-hmac/lib"
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
|
12
|
+
require 'openssl'
|
13
|
+
require 'base64'
|
14
|
+
require 'builder/xmlmarkup'
|
15
|
+
require 'nokogiri'
|
16
|
+
require 'money'
|
17
|
+
require 'net/https'
|
18
|
+
require 'active_support'
|
19
|
+
|
20
|
+
require 'duck_punches/hpricot'
|
21
|
+
require 'google-checkout/notification'
|
22
|
+
require 'google-checkout/command'
|
23
|
+
require 'google-checkout/cart'
|
24
|
+
|
25
|
+
##
|
26
|
+
# TODO
|
27
|
+
#
|
28
|
+
# * Analytics integration
|
29
|
+
# http://code.google.com/apis/checkout/developer/checkout_analytics_integration.html
|
30
|
+
|
31
|
+
module GoogleCheckout
|
32
|
+
|
33
|
+
VERSION = '0.5.1'
|
34
|
+
|
35
|
+
@@live_system = true
|
36
|
+
|
37
|
+
##
|
38
|
+
# Submit commands to the Google Checkout test servers.
|
39
|
+
|
40
|
+
def self.use_sandbox
|
41
|
+
@@live_system = false
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# The default.
|
46
|
+
|
47
|
+
def self.use_production
|
48
|
+
@@live_system = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.sandbox?
|
52
|
+
!@@live_system
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.production?
|
56
|
+
@@live_system
|
57
|
+
end
|
58
|
+
|
59
|
+
class APIError < Exception; end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,357 @@
|
|
1
|
+
|
2
|
+
module GoogleCheckout
|
3
|
+
|
4
|
+
# These are the only sizes allowed by Google. These shouldn't be needed
|
5
|
+
# by most people; just specify the :size and :buy_or_checkout options to
|
6
|
+
# Cart#checkout_button and the sizes are filled in automatically.
|
7
|
+
ButtonSizes = {
|
8
|
+
:checkout => {
|
9
|
+
:small => { :w => 160, :h => 43 },
|
10
|
+
:medium => { :w => 168, :h => 44 },
|
11
|
+
:large => { :w => 180, :h => 46 },
|
12
|
+
},
|
13
|
+
|
14
|
+
:buy_now => {
|
15
|
+
:small => { :w => 117, :h => 48 },
|
16
|
+
:medium => { :w => 121, :h => 44 },
|
17
|
+
:large => { :w => 121, :h => 44 },
|
18
|
+
},
|
19
|
+
}
|
20
|
+
|
21
|
+
##
|
22
|
+
# This class represents a cart for Google Checkout. After initializing it
|
23
|
+
# with a +merchant_id+ and +merchant_key+, you add items via add_item,
|
24
|
+
# and can then get xml via to_xml, or html code for a form that
|
25
|
+
# provides a checkout button via checkout_button.
|
26
|
+
#
|
27
|
+
# Example:
|
28
|
+
#
|
29
|
+
# item = {
|
30
|
+
# :name => 'A Quarter',
|
31
|
+
# :description => 'One shiny quarter.',
|
32
|
+
# :price => 0.25
|
33
|
+
# }
|
34
|
+
# @cart = GoogleCheckout::Cart.new(merchant_id, merchant_key, item)
|
35
|
+
# @cart.add_item(:name => "Pancakes",
|
36
|
+
# :description => "Flapjacks by mail."
|
37
|
+
# :price => 0.50,
|
38
|
+
# :quantity => 10,
|
39
|
+
# "merchant-item-id" => '2938292839')
|
40
|
+
#
|
41
|
+
# Then in your view:
|
42
|
+
#
|
43
|
+
# Checkout here! <%= @cart.checkout_button %>
|
44
|
+
#
|
45
|
+
# This object is also useful for getting back a url to the image for a Google
|
46
|
+
# Checkout button. You can use this image in forms that submit back to your own
|
47
|
+
# server for further processing via Google Checkout's level 2 XML API.
|
48
|
+
|
49
|
+
class Cart < Command
|
50
|
+
|
51
|
+
include GoogleCheckout
|
52
|
+
|
53
|
+
SANDBOX_CHECKOUT_URL = "https://sandbox.google.com/checkout/cws/v2/Merchant/%s/checkout"
|
54
|
+
PRODUCTION_CHECKOUT_URL = "https://checkout.google.com/cws/v2/Merchant/%s/checkout"
|
55
|
+
|
56
|
+
##
|
57
|
+
# You can provide extra data that will be sent to Google and returned with
|
58
|
+
# the NewOrderNotification.
|
59
|
+
#
|
60
|
+
# This should be a Hash and will be turned into XML with proper escapes.
|
61
|
+
#
|
62
|
+
# Beware using symbols as values. They may be set as sub-keys instead of values,
|
63
|
+
# so use a String or other datatype.
|
64
|
+
|
65
|
+
attr_accessor :merchant_private_data
|
66
|
+
|
67
|
+
attr_accessor :edit_cart_url
|
68
|
+
attr_accessor :continue_shopping_url
|
69
|
+
|
70
|
+
# The default options for drawing in the button that are filled in when
|
71
|
+
# checkout_button or button_url is called.
|
72
|
+
DefaultButtonOpts = {
|
73
|
+
:size => :medium,
|
74
|
+
:style => 'white',
|
75
|
+
:variant => 'text',
|
76
|
+
:loc => 'en_US',
|
77
|
+
:buy_or_checkout => nil,
|
78
|
+
}
|
79
|
+
|
80
|
+
# You need to supply, as strings, the +merchant_id+ and +merchant_key+
|
81
|
+
# used to identify your store to Google. You may optionally supply one
|
82
|
+
# or more items to put inside the cart.
|
83
|
+
def initialize(merchant_id, merchant_key, *items)
|
84
|
+
super(merchant_id, merchant_key)
|
85
|
+
@contents = []
|
86
|
+
@merchant_private_data = {}
|
87
|
+
items.each { |i| add_item i }
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# This method sets the flat rate shipping for the entire cart.
|
92
|
+
# If set, it will over ride the per product flat rate shipping.
|
93
|
+
# +frs_options+ should be a hash containing the following options:
|
94
|
+
# * price
|
95
|
+
# You may fill an some optional values as well:
|
96
|
+
# * currency (defaults to 'USD')
|
97
|
+
def flat_rate_shipping(frs_options)
|
98
|
+
# We need to check that the necessary keys are in the hash,
|
99
|
+
# Otherwise the error will happen in the middle of to_xml,
|
100
|
+
# and the bug will be harder to track.
|
101
|
+
unless frs_options.include? :price
|
102
|
+
raise ArgumentError,
|
103
|
+
"Required keys missing: :price"
|
104
|
+
end
|
105
|
+
|
106
|
+
@flat_rate_shipping = {:currency => 'USD'}.merge(frs_options)
|
107
|
+
end
|
108
|
+
|
109
|
+
def empty?
|
110
|
+
@contents.empty?
|
111
|
+
end
|
112
|
+
|
113
|
+
# Number of items in the cart.
|
114
|
+
def size
|
115
|
+
@contents.size
|
116
|
+
end
|
117
|
+
|
118
|
+
def submit_domain
|
119
|
+
(GoogleCheckout.production? ? 'checkout' : 'sandbox') + ".google.com"
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# The Google Checkout form submission url.
|
124
|
+
|
125
|
+
def submit_url
|
126
|
+
GoogleCheckout.sandbox? ? (SANDBOX_CHECKOUT_URL % @merchant_id) : (PRODUCTION_CHECKOUT_URL % @merchant_id)
|
127
|
+
end
|
128
|
+
|
129
|
+
# This method puts items in the cart.
|
130
|
+
# +item+ may be a hash, or have a method named +to_google_product+ that
|
131
|
+
# returns a hash with the required values.
|
132
|
+
# * name
|
133
|
+
# * description (a brief description as it will appear on the bill)
|
134
|
+
# * price
|
135
|
+
# You may fill in some optional values as well:
|
136
|
+
# * quantity (defaults to 1)
|
137
|
+
# * currency (defaults to 'USD')
|
138
|
+
def add_item(item)
|
139
|
+
@xml = nil
|
140
|
+
if item.respond_to? :to_google_product
|
141
|
+
item = item.to_google_product
|
142
|
+
end
|
143
|
+
|
144
|
+
# We need to check that the necessary keys are in the hash,
|
145
|
+
# Otherwise the error will happen in the middle of to_xml,
|
146
|
+
# and the bug will be harder to track.
|
147
|
+
missing_keys = [ :name, :description, :price ].select { |key|
|
148
|
+
!item.include? key
|
149
|
+
}
|
150
|
+
|
151
|
+
unless missing_keys.empty?
|
152
|
+
raise ArgumentError,
|
153
|
+
"Required keys missing: #{missing_keys.inspect}"
|
154
|
+
end
|
155
|
+
|
156
|
+
@contents << { :quantity => 1, :currency => 'USD' }.merge(item)
|
157
|
+
item
|
158
|
+
end
|
159
|
+
|
160
|
+
# This is the important method; it generatest the XML call.
|
161
|
+
# It's fairly lengthy, but trivial. It follows the docs at
|
162
|
+
# http://code.google.com/apis/checkout/developer/index.html#checkout_api
|
163
|
+
#
|
164
|
+
# It returns the raw XML string, not encoded.
|
165
|
+
def to_xml
|
166
|
+
raise RuntimeError, "Empty cart" if self.empty?
|
167
|
+
|
168
|
+
xml = Builder::XmlMarkup.new
|
169
|
+
xml.instruct!
|
170
|
+
@xml = xml.tag!('checkout-shopping-cart', :xmlns => "http://checkout.google.com/schema/2") {
|
171
|
+
xml.tag!("shopping-cart") {
|
172
|
+
xml.items {
|
173
|
+
@contents.each { |item|
|
174
|
+
xml.item {
|
175
|
+
if item.key?(:item_id)
|
176
|
+
xml.tag!('merchant-item-id', item[:item_id])
|
177
|
+
end
|
178
|
+
xml.tag!('item-name') {
|
179
|
+
xml.text! item[:name].to_s
|
180
|
+
}
|
181
|
+
xml.tag!('item-description') {
|
182
|
+
xml.text! item[:description].to_s
|
183
|
+
}
|
184
|
+
xml.tag!('unit-price', :currency => (item[:currency] || 'USD')) {
|
185
|
+
xml.text! item[:price].to_s
|
186
|
+
}
|
187
|
+
xml.quantity {
|
188
|
+
xml.text! item[:quantity].to_s
|
189
|
+
}
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
unless @merchant_private_data.empty?
|
194
|
+
xml.tag!("merchant-private-data") {
|
195
|
+
@merchant_private_data.each do |key, value|
|
196
|
+
xml.tag!(key, value)
|
197
|
+
end
|
198
|
+
}
|
199
|
+
end
|
200
|
+
}
|
201
|
+
xml.tag!('checkout-flow-support') {
|
202
|
+
xml.tag!('merchant-checkout-flow-support') {
|
203
|
+
xml.tag!('edit-cart-url', @edit_cart_url) if @edit_cart_url
|
204
|
+
xml.tag!('continue-shopping-url', @continue_shopping_url) if @continue_shopping_url
|
205
|
+
|
206
|
+
xml.tag!("request-buyer-phone-number", false)
|
207
|
+
|
208
|
+
# TODO tax-tables
|
209
|
+
xml.tag!("tax-tables") {
|
210
|
+
xml.tag!("default-tax-table") {
|
211
|
+
xml.tag!("tax-rules") {
|
212
|
+
xml.tag!("default-tax-rule") {
|
213
|
+
xml.tag!("shipping-taxed", false)
|
214
|
+
xml.tag!("rate", "0.00")
|
215
|
+
xml.tag!("tax-area") {
|
216
|
+
xml.tag!("world-area")
|
217
|
+
}
|
218
|
+
}
|
219
|
+
}
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
# TODO Shipping calculations
|
224
|
+
# These are currently hard-coded for PeepCode.
|
225
|
+
# Does anyone care to send a patch to enhance
|
226
|
+
# this for more flexibility?
|
227
|
+
xml.tag!('shipping-methods') {
|
228
|
+
xml.tag!('pickup', :name =>'Shipping') {
|
229
|
+
xml.tag!('price', "2.00", :currency => currency)
|
230
|
+
}
|
231
|
+
}
|
232
|
+
}
|
233
|
+
}
|
234
|
+
}
|
235
|
+
@xml.dup
|
236
|
+
end
|
237
|
+
|
238
|
+
# Generates the XML for the shipping cost, conditional on
|
239
|
+
# @flat_rate_shipping being set.
|
240
|
+
def shipping_cost_xml
|
241
|
+
xml = Builder::XmlMarkup.new
|
242
|
+
if @flat_rate_shipping
|
243
|
+
xml.price(:currency => currency) {
|
244
|
+
xml.text! @flat_rate_shipping[:price].to_s
|
245
|
+
}
|
246
|
+
else
|
247
|
+
xml.price(:currency => @currency) {
|
248
|
+
xml.text! shipping_cost.to_s
|
249
|
+
}
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns the shipping cost for the contents of the cart.
|
254
|
+
def shipping_cost
|
255
|
+
currency = 'USD'
|
256
|
+
shipping = @contents.inject(0) { |total,item|
|
257
|
+
total + item[:regular_shipping].to_i
|
258
|
+
}.to_s
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns the currency for the cart. Mixing currency not allowed; this
|
262
|
+
# library can't convert between currencies.
|
263
|
+
def currency
|
264
|
+
# Mixing currency not allowed; this
|
265
|
+
# library can't convert between
|
266
|
+
# currencies.
|
267
|
+
currencies = @contents.map { |item| item[:currency] }.uniq || "USD"
|
268
|
+
|
269
|
+
case currencies.count
|
270
|
+
when 0
|
271
|
+
"USD"
|
272
|
+
when 1
|
273
|
+
currencies.first
|
274
|
+
else
|
275
|
+
raise RuntimeError.new("Mixing currency not allowed")
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns the signature for the cart XML.
|
281
|
+
def signature
|
282
|
+
@xml or to_xml
|
283
|
+
|
284
|
+
digest = OpenSSL::Digest::Digest.new('sha1')
|
285
|
+
OpenSSL::HMAC.digest(digest, @merchant_key, @xml)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Returns HTML for a checkout form for buying all the items in the
|
289
|
+
# cart.
|
290
|
+
def checkout_button(button_opts = {})
|
291
|
+
@xml or to_xml
|
292
|
+
burl = button_url(button_opts)
|
293
|
+
html = Builder::XmlMarkup.new(:indent => 2)
|
294
|
+
html.form({
|
295
|
+
:action => submit_url,
|
296
|
+
:style => 'border: 0;',
|
297
|
+
:id => 'BB_BuyButtonForm',
|
298
|
+
:method => 'post',
|
299
|
+
:name => 'BB_BuyButtonForm'
|
300
|
+
}) do
|
301
|
+
html.input({
|
302
|
+
:name => 'cart',
|
303
|
+
:type => 'hidden',
|
304
|
+
:value => Base64.encode64(@xml).gsub("\n", '')
|
305
|
+
})
|
306
|
+
html.input({
|
307
|
+
:name => 'signature',
|
308
|
+
:type => 'hidden',
|
309
|
+
:value => Base64.encode64(signature).gsub("\n", '')
|
310
|
+
})
|
311
|
+
html.input({
|
312
|
+
:alt => 'Google Checkout',
|
313
|
+
:style => "width: auto;",
|
314
|
+
:src => button_url(button_opts),
|
315
|
+
:type => 'image'
|
316
|
+
})
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Given a set of options for the button, button_url returns the URL
|
321
|
+
# for the button image.
|
322
|
+
# The options are the same as those specified on
|
323
|
+
# http://checkout.google.com/seller/checkout_buttons.html , with a
|
324
|
+
# couple of extra options for convenience. Rather than specifying the
|
325
|
+
# width and height manually, you may specify :size to be one of :small,
|
326
|
+
# :medium, or :large, and that you may set :buy_or_checkout to :buy_now
|
327
|
+
# or :checkout to get a 'Buy Now' button versus a 'Checkout' button. If
|
328
|
+
# you don't specify :buy_or_checkout, the Cart will try to guess based
|
329
|
+
# on if the cart has more than one item in it. Whatever you don't pass
|
330
|
+
# will be filled in with the defaults from DefaultButtonOpts.
|
331
|
+
#
|
332
|
+
# http://checkout.google.com/buttons/checkout.gif
|
333
|
+
# http://sandbox.google.com/checkout/buttons/checkout.gif
|
334
|
+
|
335
|
+
def button_url(opts = {})
|
336
|
+
opts = DefaultButtonOpts.merge opts
|
337
|
+
opts[:buy_or_checkout] ||= @contents.size > 1 ? :checkout : :buy_now
|
338
|
+
opts.merge! ButtonSizes[opts[:buy_or_checkout]][opts[:size]]
|
339
|
+
bname = opts[:buy_or_checkout] == :buy_now ? 'buy.gif' : 'checkout.gif'
|
340
|
+
opts.delete :size
|
341
|
+
opts.delete :buy_or_checkout
|
342
|
+
opts[:merchant_id] = @merchant_id
|
343
|
+
|
344
|
+
path = opts.map { |k,v| "#{k}=#{v}" }.join('&')
|
345
|
+
|
346
|
+
# HACK Sandbox graphics are in the checkout subdirectory
|
347
|
+
subdir = ""
|
348
|
+
if GoogleCheckout.sandbox? && bname == "checkout.gif"
|
349
|
+
subdir = "checkout/"
|
350
|
+
end
|
351
|
+
|
352
|
+
# TODO Use /checkout/buttons/checkout.gif if in sandbox.
|
353
|
+
"https://#{submit_domain}/#{ subdir }buttons/#{bname}?#{path}"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
|
2
|
+
# TODO
|
3
|
+
#
|
4
|
+
# * Use standard ssl certs
|
5
|
+
|
6
|
+
module GoogleCheckout
|
7
|
+
|
8
|
+
##
|
9
|
+
# Abstract class for commands.
|
10
|
+
#
|
11
|
+
# https://sandbox.google.com/checkout/cws/v2/Merchant/1234567890/request
|
12
|
+
# https://checkout.google.com/cws/v2/Merchant/1234567890/request
|
13
|
+
|
14
|
+
class Command
|
15
|
+
|
16
|
+
attr_accessor :merchant_id, :merchant_key, :currency
|
17
|
+
|
18
|
+
SANDBOX_REQUEST_URL = "https://sandbox.google.com/checkout/cws/v2/Merchant/%s/request"
|
19
|
+
PRODUCTION_REQUEST_URL = "https://checkout.google.com/cws/v2/Merchant/%s/request"
|
20
|
+
|
21
|
+
def initialize(merchant_id, merchant_key)
|
22
|
+
@merchant_id = merchant_id
|
23
|
+
@merchant_key = merchant_key
|
24
|
+
|
25
|
+
@currency = "USD"
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Returns the appropriate sandbox or production url for posting API requests.
|
30
|
+
|
31
|
+
def url
|
32
|
+
GoogleCheckout.sandbox? ? (SANDBOX_REQUEST_URL % @merchant_id) : (PRODUCTION_REQUEST_URL % @merchant_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Sends the Command's XML to GoogleCheckout via HTTPS with Basic Auth.
|
37
|
+
#
|
38
|
+
# Returns a GoogleCheckout::RequestReceived or a GoogleCheckout::Error object.
|
39
|
+
|
40
|
+
def post
|
41
|
+
# Create HTTP(S) POST command and set up Basic Authentication.
|
42
|
+
uri = URI.parse(url)
|
43
|
+
|
44
|
+
request = Net::HTTP::Post.new(uri.path)
|
45
|
+
request.basic_auth(@merchant_id, @merchant_key)
|
46
|
+
|
47
|
+
# Set up the HTTP connection object and the SSL layer.
|
48
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
49
|
+
https.use_ssl = true
|
50
|
+
https.cert_store = self.class.x509_store
|
51
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
52
|
+
https.verify_depth = 5
|
53
|
+
|
54
|
+
# Send the request to Google.
|
55
|
+
response = https.request(request, self.to_xml)
|
56
|
+
|
57
|
+
# NOTE Because Notification.parse() is used, the content of objects
|
58
|
+
# will be correctly parsed no matter what the HTTP response code
|
59
|
+
# is from the server.
|
60
|
+
case response
|
61
|
+
when Net::HTTPSuccess, Net::HTTPClientError
|
62
|
+
notification = Notification.parse(response.body)
|
63
|
+
if notification.error?
|
64
|
+
raise APIError, "#{notification.message} [in #{GoogleCheckout.production? ? 'production' : 'sandbox' }]"
|
65
|
+
end
|
66
|
+
return notification
|
67
|
+
when Net::HTTPRedirection, Net::HTTPServerError, Net::HTTPInformation
|
68
|
+
raise "Unexpected response code (#{response.class}): #{response.code} - #{response.message}"
|
69
|
+
else
|
70
|
+
raise "Unknown response code: #{response.code} - #{response.message}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Class method to return the OpenSSL::X509::Store instance for the
|
76
|
+
# CA certificates.
|
77
|
+
#
|
78
|
+
# NOTE May not be thread-safe.
|
79
|
+
|
80
|
+
def self.x509_store
|
81
|
+
return @@x509_store if defined?(@@x509_store)
|
82
|
+
|
83
|
+
cacert_path = File.expand_path(File.dirname(__FILE__) + '/../../support/cacert.pem')
|
84
|
+
|
85
|
+
@@x509_store = OpenSSL::X509::Store.new
|
86
|
+
@@x509_store.add_file(cacert_path)
|
87
|
+
|
88
|
+
return @@x509_store
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Abstract class for all commands associated with an existing order.
|
95
|
+
|
96
|
+
class OrderCommand < Command
|
97
|
+
|
98
|
+
attr_accessor :google_order_number, :amount
|
99
|
+
|
100
|
+
##
|
101
|
+
# Make a new object. Last argument is the Google's order number as received
|
102
|
+
# in the NewOrderNotification.
|
103
|
+
|
104
|
+
def initialize(merchant_id, merchant_key, google_order_number)
|
105
|
+
# TODO raise "Not an order number!" unless google_order_number.is_a? String
|
106
|
+
super(merchant_id, merchant_key)
|
107
|
+
@google_order_number = google_order_number
|
108
|
+
@amount = 0.00
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Create a new ChargeOrder object, set the +amount+, then
|
115
|
+
# +post+ it.
|
116
|
+
|
117
|
+
class ChargeOrder < OrderCommand
|
118
|
+
|
119
|
+
def to_xml
|
120
|
+
raise "Charge amount must be greater than 0!" unless @amount.to_f > 0.0
|
121
|
+
|
122
|
+
xml = Builder::XmlMarkup.new
|
123
|
+
xml.instruct!
|
124
|
+
@xml = xml.tag!('charge-order', {
|
125
|
+
:xmlns => "http://checkout.google.com/schema/2",
|
126
|
+
"google-order-number" => @google_order_number
|
127
|
+
}) do
|
128
|
+
xml.tag!("amount", @amount, {:currency => @currency})
|
129
|
+
end
|
130
|
+
@xml
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Tells Google that the order has shipped.
|
137
|
+
|
138
|
+
class DeliverOrder < OrderCommand
|
139
|
+
|
140
|
+
def to_xml
|
141
|
+
|
142
|
+
xml = Builder::XmlMarkup.new
|
143
|
+
xml.instruct!
|
144
|
+
@xml = xml.tag!('deliver-order', {
|
145
|
+
:xmlns => "http://checkout.google.com/schema/2",
|
146
|
+
"google-order-number" => @google_order_number
|
147
|
+
}) do
|
148
|
+
xml.tag!("send-email", false)
|
149
|
+
end
|
150
|
+
@xml
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Send a message to the buyer associated with an order.
|
157
|
+
#
|
158
|
+
# Google will actually send the message to their email address.
|
159
|
+
|
160
|
+
class SendBuyerMessage < OrderCommand
|
161
|
+
|
162
|
+
##
|
163
|
+
# Make a new message to send.
|
164
|
+
#
|
165
|
+
# The last argument is the actual message.
|
166
|
+
#
|
167
|
+
# Call +post+ on the resulting object to submit it to Google for sending.
|
168
|
+
|
169
|
+
def initialize(merchant_id, merchant_key, google_order_number, message)
|
170
|
+
# TODO Raise meaninful error if message is longer than 255 characters
|
171
|
+
raise "Google won't send anything longer than 255 characters! Sorry!" if message.length > 255
|
172
|
+
@message = message
|
173
|
+
super(merchant_id, merchant_key, google_order_number)
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_xml # :nodoc:
|
177
|
+
xml = Builder::XmlMarkup.new
|
178
|
+
xml.instruct!
|
179
|
+
@xml = xml.tag!('send-buyer-message', {
|
180
|
+
:xmlns => "http://checkout.google.com/schema/2",
|
181
|
+
"google-order-number" => @google_order_number
|
182
|
+
}) do
|
183
|
+
xml.tag!("message", @message)
|
184
|
+
xml.tag!("send-email", true)
|
185
|
+
end
|
186
|
+
@xml
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|