codehog-google-checkout 1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/History.txt +25 -0
  2. data/Isolate +13 -0
  3. data/MIT-LICENSE.txt +23 -0
  4. data/Manifest.txt +42 -0
  5. data/README.txt +148 -0
  6. data/Rakefile +35 -0
  7. data/examples/google_notifications_controller.rb +159 -0
  8. data/lib/duck_punches/hpricot.rb +24 -0
  9. data/lib/google-checkout.rb +61 -0
  10. data/lib/google-checkout/cart.rb +357 -0
  11. data/lib/google-checkout/command.rb +191 -0
  12. data/lib/google-checkout/notification.rb +226 -0
  13. data/spec/fixtures/google/checkout-shopping-cart.xml +22 -0
  14. data/spec/fixtures/google/commands/add-merchant-order-number.xml +5 -0
  15. data/spec/fixtures/google/commands/add-tracking-data.xml +8 -0
  16. data/spec/fixtures/google/commands/archive-order.xml +3 -0
  17. data/spec/fixtures/google/commands/authorize-order.xml +2 -0
  18. data/spec/fixtures/google/commands/cancel-order.xml +5 -0
  19. data/spec/fixtures/google/commands/charge-order.xml +4 -0
  20. data/spec/fixtures/google/commands/deliver-order.xml +9 -0
  21. data/spec/fixtures/google/commands/process-order.xml +2 -0
  22. data/spec/fixtures/google/commands/refund-order.xml +6 -0
  23. data/spec/fixtures/google/commands/send-buyer-message.xml +7 -0
  24. data/spec/fixtures/google/commands/unarchive-order.xml +2 -0
  25. data/spec/fixtures/google/notifications/authorization-amount-notification.xml +10 -0
  26. data/spec/fixtures/google/notifications/charge-amount-notification.xml +8 -0
  27. data/spec/fixtures/google/notifications/chargeback-amount-notification.xml +8 -0
  28. data/spec/fixtures/google/notifications/new-order-notification.xml +85 -0
  29. data/spec/fixtures/google/notifications/order-state-change-notification.xml +11 -0
  30. data/spec/fixtures/google/notifications/refund-amount-notification.xml +8 -0
  31. data/spec/fixtures/google/notifications/risk-information-notification.xml +23 -0
  32. data/spec/fixtures/google/responses/checkout-redirect.xml +5 -0
  33. data/spec/fixtures/google/responses/error.xml +5 -0
  34. data/spec/fixtures/google/responses/request-received.xml +3 -0
  35. data/spec/google-checkout/cart_spec.rb +165 -0
  36. data/spec/google-checkout/command_spec.rb +131 -0
  37. data/spec/google-checkout/notification_spec.rb +181 -0
  38. data/spec/google-checkout/response_spec.rb +49 -0
  39. data/spec/google-checkout_spec.rb +15 -0
  40. data/spec/spec_helper.rb +47 -0
  41. data/support/cacert.pem +7815 -0
  42. 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