mlins-google-checkout 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/History.txt +14 -0
  2. data/MIT-LICENSE.txt +23 -0
  3. data/README.txt +143 -0
  4. data/Rakefile +34 -0
  5. data/VERSION.yml +4 -0
  6. data/examples/google_notifications_controller.rb +159 -0
  7. data/lib/duck_punches/hpricot.rb +24 -0
  8. data/lib/google-checkout.rb +65 -0
  9. data/lib/google-checkout/cart.rb +302 -0
  10. data/lib/google-checkout/command.rb +191 -0
  11. data/lib/google-checkout/geography.rb +7 -0
  12. data/lib/google-checkout/geography/area.rb +11 -0
  13. data/lib/google-checkout/geography/postal.rb +26 -0
  14. data/lib/google-checkout/geography/us_country.rb +24 -0
  15. data/lib/google-checkout/geography/us_state.rb +22 -0
  16. data/lib/google-checkout/geography/us_zip.rb +22 -0
  17. data/lib/google-checkout/geography/world.rb +12 -0
  18. data/lib/google-checkout/merchant_calculation.rb +30 -0
  19. data/lib/google-checkout/notification.rb +212 -0
  20. data/lib/google-checkout/shipping.rb +8 -0
  21. data/lib/google-checkout/shipping/filters.rb +32 -0
  22. data/lib/google-checkout/shipping/flat_rate.rb +26 -0
  23. data/lib/google-checkout/shipping/merchant_calculated.rb +29 -0
  24. data/lib/google-checkout/shipping/method.rb +11 -0
  25. data/lib/google-checkout/shipping/pickup.rb +22 -0
  26. data/lib/google-checkout/shipping/restrictions.rb +32 -0
  27. data/spec/fixtures/google/checkout-shopping-cart.xml +22 -0
  28. data/spec/fixtures/google/commands/add-merchant-order-number.xml +5 -0
  29. data/spec/fixtures/google/commands/add-tracking-data.xml +8 -0
  30. data/spec/fixtures/google/commands/archive-order.xml +3 -0
  31. data/spec/fixtures/google/commands/authorize-order.xml +2 -0
  32. data/spec/fixtures/google/commands/cancel-order.xml +5 -0
  33. data/spec/fixtures/google/commands/charge-order.xml +4 -0
  34. data/spec/fixtures/google/commands/deliver-order.xml +9 -0
  35. data/spec/fixtures/google/commands/process-order.xml +2 -0
  36. data/spec/fixtures/google/commands/refund-order.xml +6 -0
  37. data/spec/fixtures/google/commands/send-buyer-message.xml +7 -0
  38. data/spec/fixtures/google/commands/unarchive-order.xml +2 -0
  39. data/spec/fixtures/google/merchant_calculations/shipping.xml +40 -0
  40. data/spec/fixtures/google/notifications/authorization-amount-notification.xml +10 -0
  41. data/spec/fixtures/google/notifications/charge-amount-notification.xml +8 -0
  42. data/spec/fixtures/google/notifications/chargeback-amount-notification.xml +8 -0
  43. data/spec/fixtures/google/notifications/new-order-notification.xml +85 -0
  44. data/spec/fixtures/google/notifications/order-state-change-notification.xml +11 -0
  45. data/spec/fixtures/google/notifications/refund-amount-notification.xml +8 -0
  46. data/spec/fixtures/google/notifications/risk-information-notification.xml +23 -0
  47. data/spec/fixtures/google/responses/checkout-redirect.xml +5 -0
  48. data/spec/fixtures/google/responses/error.xml +5 -0
  49. data/spec/fixtures/google/responses/request-received.xml +3 -0
  50. data/spec/google-checkout/cart_spec.rb +110 -0
  51. data/spec/google-checkout/command_spec.rb +131 -0
  52. data/spec/google-checkout/geography/postal_spec.rb +26 -0
  53. data/spec/google-checkout/geography/us_country_spec.rb +26 -0
  54. data/spec/google-checkout/geography/us_state_spec.rb +11 -0
  55. data/spec/google-checkout/geography/us_zip_spec.rb +11 -0
  56. data/spec/google-checkout/geography/world_spec.rb +12 -0
  57. data/spec/google-checkout/merchant_calculation_spec.rb +17 -0
  58. data/spec/google-checkout/notification_spec.rb +175 -0
  59. data/spec/google-checkout/response_spec.rb +49 -0
  60. data/spec/google-checkout/shipping/flat_rate_spec.rb +46 -0
  61. data/spec/google-checkout/shipping/merchant_calculated_spec.rb +70 -0
  62. data/spec/google-checkout/shipping/pickup_spec.rb +22 -0
  63. data/spec/google-checkout_spec.rb +15 -0
  64. data/spec/spec_helper.rb +47 -0
  65. data/support/cacert.pem +7815 -0
  66. metadata +140 -0
@@ -0,0 +1,302 @@
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
+ attr_accessor :merchant_calculations_url
71
+
72
+ attr_accessor :shipping_methods
73
+
74
+ # The default options for drawing in the button that are filled in when
75
+ # checkout_button or button_url is called.
76
+ DefaultButtonOpts = {
77
+ :size => :medium,
78
+ :style => 'white',
79
+ :variant => 'text',
80
+ :loc => 'en_US',
81
+ :buy_or_checkout => nil,
82
+ }
83
+
84
+ # You need to supply, as strings, the +merchant_id+ and +merchant_key+
85
+ # used to identify your store to Google. You may optionally supply one
86
+ # or more items to put inside the cart.
87
+ def initialize(merchant_id, merchant_key, *items)
88
+ super(merchant_id, merchant_key)
89
+ @contents = []
90
+ @merchant_private_data = {}
91
+ @shipping_methods = []
92
+ items.each { |i| add_item i }
93
+ end
94
+
95
+ def empty?
96
+ @contents.empty?
97
+ end
98
+
99
+ # Number of items in the cart.
100
+ def size
101
+ @contents.size
102
+ end
103
+
104
+ def submit_domain
105
+ (GoogleCheckout.production? ? 'checkout' : 'sandbox') + ".google.com"
106
+ end
107
+
108
+ ##
109
+ # The Google Checkout form submission url.
110
+
111
+ def submit_url
112
+ GoogleCheckout.sandbox? ? (SANDBOX_CHECKOUT_URL % @merchant_id) : (PRODUCTION_CHECKOUT_URL % @merchant_id)
113
+ end
114
+
115
+ # This method puts items in the cart.
116
+ # +item+ may be a hash, or have a method named +to_google_product+ that
117
+ # returns a hash with the required values.
118
+ # * name
119
+ # * description (a brief description as it will appear on the bill)
120
+ # * price
121
+ # You may fill in some optional values as well:
122
+ # * quantity (defaults to 1)
123
+ # * currency (defaults to 'USD')
124
+ def add_item(item)
125
+ @xml = nil
126
+ if item.respond_to? :to_google_product
127
+ item = item.to_google_product
128
+ end
129
+
130
+ # We need to check that the necessary keys are in the hash,
131
+ # Otherwise the error will happen in the middle of to_xml,
132
+ # and the bug will be harder to track.
133
+ missing_keys = [ :name, :description, :price ].select { |key|
134
+ !item.include? key
135
+ }
136
+
137
+ unless missing_keys.empty?
138
+ raise ArgumentError,
139
+ "Required keys missing: #{missing_keys.inspect}"
140
+ end
141
+
142
+ @contents << { :quantity => 1, :currency => 'USD' }.merge(item)
143
+ item
144
+ end
145
+
146
+ # This is the important method; it generatest the XML call.
147
+ # It's fairly lengthy, but trivial. It follows the docs at
148
+ # http://code.google.com/apis/checkout/developer/index.html#checkout_api
149
+ #
150
+ # It returns the raw XML string, not encoded.
151
+ def to_xml
152
+ raise RuntimeError, "Empty cart" if self.empty?
153
+
154
+ xml = Builder::XmlMarkup.new
155
+ xml.instruct!
156
+ @xml = xml.tag!('checkout-shopping-cart', :xmlns => "http://checkout.google.com/schema/2") {
157
+ xml.tag!("shopping-cart") {
158
+ xml.items {
159
+ @contents.each { |item|
160
+ xml.item {
161
+ if item.key?(:item_id)
162
+ xml.tag!('merchant-item-id', item[:item_id])
163
+ end
164
+ xml.tag!('item-name') {
165
+ xml.text! item[:name].to_s
166
+ }
167
+ xml.tag!('item-description') {
168
+ xml.text! item[:description].to_s
169
+ }
170
+ xml.tag!('unit-price', :currency => (item[:currency] || 'USD')) {
171
+ xml.text! item[:price].to_s
172
+ }
173
+ xml.quantity {
174
+ xml.text! item[:quantity].to_s
175
+ }
176
+ xml.tag!('merchant-private-item-data') {
177
+ xml << item[:merchant_private_item_data]
178
+ } if item.key?(:merchant_private_item_data)
179
+ }
180
+ }
181
+ }
182
+ unless @merchant_private_data.empty?
183
+ xml.tag!("merchant-private-data") {
184
+ @merchant_private_data.each do |key, value|
185
+ xml.tag!(key, value)
186
+ end
187
+ }
188
+ end
189
+ }
190
+ xml.tag!('checkout-flow-support') {
191
+ xml.tag!('merchant-checkout-flow-support') {
192
+ xml.tag!('edit-cart-url', @edit_cart_url) if @edit_cart_url
193
+ xml.tag!('continue-shopping-url', @continue_shopping_url) if @continue_shopping_url
194
+ xml.tag!("request-buyer-phone-number", false)
195
+ xml.tag!('merchant-calculations') {
196
+ xml.tag!('merchant-calculations-url') {
197
+ xml.text! @merchant_calculations_url
198
+ }
199
+ } if @merchant_calculations_url
200
+
201
+ # TODO tax-tables
202
+ xml.tag!("tax-tables") {
203
+ xml.tag!("default-tax-table") {
204
+ xml.tag!("tax-rules") {
205
+ xml.tag!("default-tax-rule") {
206
+ xml.tag!("shipping-taxed", false)
207
+ xml.tag!("rate", "0.00")
208
+ xml.tag!("tax-area") {
209
+ xml.tag!("world-area")
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ xml.tag!('shipping-methods') {
217
+ @shipping_methods.each do |shipping_method|
218
+ xml << shipping_method.to_xml
219
+ end
220
+ }
221
+ }
222
+ }
223
+ }
224
+ @xml.dup
225
+ end
226
+
227
+ # Returns the signature for the cart XML.
228
+ def signature
229
+ @xml or to_xml
230
+ HMAC::SHA1.digest(@merchant_key, @xml)
231
+ end
232
+
233
+ # Returns HTML for a checkout form for buying all the items in the
234
+ # cart.
235
+ def checkout_button(button_opts = {})
236
+ @xml or to_xml
237
+ burl = button_url(button_opts)
238
+ html = Builder::XmlMarkup.new(:indent => 2)
239
+ html.form({
240
+ :action => submit_url,
241
+ :style => 'border: 0;',
242
+ :id => 'BB_BuyButtonForm',
243
+ :method => 'post',
244
+ :name => 'BB_BuyButtonForm'
245
+ }) do
246
+ html.input({
247
+ :name => 'cart',
248
+ :type => 'hidden',
249
+ :value => Base64.encode64(@xml).gsub("\n", '')
250
+ })
251
+ html.input({
252
+ :name => 'signature',
253
+ :type => 'hidden',
254
+ :value => Base64.encode64(signature).gsub("\n", '')
255
+ })
256
+ html.input({
257
+ :alt => 'Google Checkout',
258
+ :style => "width: auto;",
259
+ :src => button_url(button_opts),
260
+ :type => 'image'
261
+ })
262
+ end
263
+ end
264
+
265
+ # Given a set of options for the button, button_url returns the URL
266
+ # for the button image.
267
+ # The options are the same as those specified on
268
+ # http://checkout.google.com/seller/checkout_buttons.html , with a
269
+ # couple of extra options for convenience. Rather than specifying the
270
+ # width and height manually, you may specify :size to be one of :small,
271
+ # :medium, or :large, and that you may set :buy_or_checkout to :buy_now
272
+ # or :checkout to get a 'Buy Now' button versus a 'Checkout' button. If
273
+ # you don't specify :buy_or_checkout, the Cart will try to guess based
274
+ # on if the cart has more than one item in it. Whatever you don't pass
275
+ # will be filled in with the defaults from DefaultButtonOpts.
276
+ #
277
+ # http://checkout.google.com/buttons/checkout.gif
278
+ # http://sandbox.google.com/checkout/buttons/checkout.gif
279
+
280
+ def button_url(opts = {})
281
+ opts = DefaultButtonOpts.merge opts
282
+ opts[:buy_or_checkout] ||= @contents.size > 1 ? :checkout : :buy_now
283
+ opts.merge! ButtonSizes[opts[:buy_or_checkout]][opts[:size]]
284
+ bname = opts[:buy_or_checkout] == :buy_now ? 'buy.gif' : 'checkout.gif'
285
+ opts.delete :size
286
+ opts.delete :buy_or_checkout
287
+ opts[:merchant_id] = @merchant_id
288
+
289
+ path = opts.map { |k,v| "#{k}=#{v}" }.join('&')
290
+
291
+ # HACK Sandbox graphics are in the checkout subdirectory
292
+ subdir = ""
293
+ if GoogleCheckout.sandbox? && bname == "checkout.gif"
294
+ subdir = "checkout/"
295
+ end
296
+
297
+ # TODO Use /checkout/buttons/checkout.gif if in sandbox.
298
+ "http://#{submit_domain}/#{ subdir }buttons/#{bname}?#{path}"
299
+ end
300
+ end
301
+
302
+ 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