codehog-google-checkout 1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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