Class: GoogleCheckout::Cart

Inherits:
Command
  • Object
show all
Includes:
GoogleCheckout
Defined in:
lib/google-checkout/cart.rb

Overview

This class represents a cart for Google Checkout. After initializing it with a merchant_id and merchant_key, you add items via add_item, and can then get xml via to_xml, or html code for a form that provides a checkout button via checkout_button.

Example:

item = {
         :name => 'A Quarter',
         :description => 'One shiny quarter.',
         :price => 0.25
       }
@cart = GoogleCheckout::Cart.new(merchant_id, merchant_key, item)
@cart.add_item(:name => "Pancakes",
               :description => "Flapjacks by mail."
               :price => 0.50,
               :quantity => 10,
               "merchant-item-id" => '2938292839')

Then in your view:

Checkout here! <%= @cart.checkout_button %>

This object is also useful for getting back a url to the image for a Google Checkout button. You can use this image in forms that submit back to your own server for further processing via Google Checkout’s level 2 XML API.

Constant Summary collapse

SANDBOX_CHECKOUT_URL =
"https://sandbox.google.com/checkout/cws/v2/Merchant/%s/checkout"
PRODUCTION_CHECKOUT_URL =
"https://checkout.google.com/cws/v2/Merchant/%s/checkout"
DefaultButtonOpts =

The default options for drawing in the button that are filled in when checkout_button or button_url is called.

{
  :size => :medium,
  :style => 'white',
  :variant => 'text',
  :loc => 'en_US',
  :buy_or_checkout => nil,
}

Constants included from GoogleCheckout

ButtonSizes, VERSION

Constants inherited from Command

GoogleCheckout::Command::PRODUCTION_REQUEST_URL, GoogleCheckout::Command::SANDBOX_REQUEST_URL

Instance Attribute Summary collapse

Attributes inherited from Command

#merchant_id, #merchant_key

Instance Method Summary collapse

Methods included from GoogleCheckout

production?, sandbox?, use_production, use_sandbox

Methods inherited from Command

#post, #url, x509_store

Constructor Details

#initialize(merchant_id, merchant_key, *items) ⇒ Cart

You need to supply, as strings, the merchant_id and merchant_key used to identify your store to Google. You may optionally supply one or more items to put inside the cart.



83
84
85
86
87
88
# File 'lib/google-checkout/cart.rb', line 83

def initialize(merchant_id, merchant_key, *items)
  super(merchant_id, merchant_key)
  @contents = []
  @merchant_private_data = {}
  items.each { |i| add_item i }
end

Instance Attribute Details

#continue_shopping_urlObject

Returns the value of attribute continue_shopping_url.



68
69
70
# File 'lib/google-checkout/cart.rb', line 68

def continue_shopping_url
  @continue_shopping_url
end

#edit_cart_urlObject

Returns the value of attribute edit_cart_url.



67
68
69
# File 'lib/google-checkout/cart.rb', line 67

def edit_cart_url
  @edit_cart_url
end

#merchant_private_dataObject

You can provide extra data that will be sent to Google and returned with the NewOrderNotification.

This should be a Hash and will be turned into XML with proper escapes.

Beware using symbols as values. They may be set as sub-keys instead of values, so use a String or other datatype.



65
66
67
# File 'lib/google-checkout/cart.rb', line 65

def merchant_private_data
  @merchant_private_data
end

Instance Method Details

#add_item(item) ⇒ Object

This method puts items in the cart. item may be a hash, or have a method named to_google_product that returns a hash with the required values.

  • name

  • description (a brief description as it will appear on the bill)

  • price

You may fill in some optional values as well:

  • quantity (defaults to 1)

  • currency (defaults to ‘USD’)



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/google-checkout/cart.rb', line 138

def add_item(item)
  @xml = nil
  if item.respond_to? :to_google_product
    item = item.to_google_product
  end

  # We need to check that the necessary keys are in the hash,
  # Otherwise the error will happen in the middle of to_xml,
  # and the bug will be harder to track.
  missing_keys = [ :name, :description, :price ].select { |key|
    !item.include? key
  }

  unless missing_keys.empty?
    raise ArgumentError,
    "Required keys missing: #{missing_keys.inspect}"
  end

  @contents << { :quantity => 1, :currency => 'USD' }.merge(item)
  item
end

#button_url(opts = {}) ⇒ Object

Given a set of options for the button, button_url returns the URL for the button image. The options are the same as those specified on checkout.google.com/seller/checkout_buttons.html , with a couple of extra options for convenience. Rather than specifying the width and height manually, you may specify :size to be one of :small, :medium, or :large, and that you may set :buy_or_checkout to :buy_now or :checkout to get a ‘Buy Now’ button versus a ‘Checkout’ button. If you don’t specify :buy_or_checkout, the Cart will try to guess based on if the cart has more than one item in it. Whatever you don’t pass will be filled in with the defaults from DefaultButtonOpts.

http://checkout.google.com/buttons/checkout.gif
http://sandbox.google.com/checkout/buttons/checkout.gif


335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/google-checkout/cart.rb', line 335

def button_url(opts = {})
  opts = DefaultButtonOpts.merge opts
  opts[:buy_or_checkout] ||= @contents.size > 1 ? :checkout : :buy_now
  opts.merge! ButtonSizes[opts[:buy_or_checkout]][opts[:size]]
  bname = opts[:buy_or_checkout] == :buy_now ? 'buy.gif' : 'checkout.gif'
  opts.delete :size
  opts.delete :buy_or_checkout
  opts[:merchant_id] = @merchant_id

  path = opts.map { |k,v| "#{k}=#{v}" }.join('&')

  # HACK Sandbox graphics are in the checkout subdirectory
  subdir = ""
  if GoogleCheckout.sandbox? && bname == "checkout.gif"
    subdir = "checkout/"
  end

  # TODO Use /checkout/buttons/checkout.gif if in sandbox.
  "https://#{submit_domain}/#{ subdir }buttons/#{bname}?#{path}"
end

#checkout_button(button_opts = {}) ⇒ Object

Returns HTML for a checkout form for buying all the items in the cart.



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/google-checkout/cart.rb', line 290

def checkout_button(button_opts = {})
  @xml or to_xml
  burl = button_url(button_opts)
  html = Builder::XmlMarkup.new(:indent => 2)
  html.form({
              :action => submit_url,
              :style => 'border: 0;',
              :id => 'BB_BuyButtonForm',
              :method => 'post',
              :name => 'BB_BuyButtonForm'
            }) do
    html.input({
                 :name => 'cart',
                 :type => 'hidden',
                 :value => Base64.encode64(@xml).gsub("\n", '')
               })
    html.input({
                 :name => 'signature',
                 :type => 'hidden',
                 :value => Base64.encode64(signature).gsub("\n", '')
               })
    html.input({
                 :alt => 'Google Checkout',
                 :style => "width: auto;",
                 :src => button_url(button_opts),
                 :type => 'image'
               })
  end
end

#currencyObject

Returns the currency for the cart. Mixing currency not allowed; this library can’t convert between currencies.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/google-checkout/cart.rb', line 263

def currency
  # Mixing currency not allowed; this
  # library can't convert between
  # currencies.
  currencies = @contents.map { |item| item[:currency] }.uniq || "USD"

  case currencies.count
    when 0
      "USD"
    when 1
      currencies.first
    else
      raise RuntimeError.new("Mixing currency not allowed")
  end

end

#empty?Boolean

Returns:

  • (Boolean)


109
110
111
# File 'lib/google-checkout/cart.rb', line 109

def empty?
  @contents.empty?
end

#flat_rate_shipping(frs_options) ⇒ Object

This method sets the flat rate shipping for the entire cart. If set, it will over ride the per product flat rate shipping. frs_options should be a hash containing the following options:

  • price

You may fill an some optional values as well:

  • currency (defaults to ‘USD’)



97
98
99
100
101
102
103
104
105
106
107
# File 'lib/google-checkout/cart.rb', line 97

def flat_rate_shipping(frs_options)
  # We need to check that the necessary keys are in the hash,
  # Otherwise the error will happen in the middle of to_xml,
  # and the bug will be harder to track.
  unless frs_options.include? :price
    raise ArgumentError,
    "Required keys missing: :price"
  end

  @flat_rate_shipping = {:currency => 'USD'}.merge(frs_options)
end

#shipping_costObject

Returns the shipping cost for the contents of the cart.



254
255
256
257
258
259
# File 'lib/google-checkout/cart.rb', line 254

def shipping_cost
  currency = 'USD'
  shipping = @contents.inject(0) { |total,item|
    total + item[:regular_shipping].to_i
  }.to_s
end

#shipping_cost_xmlObject

Generates the XML for the shipping cost, conditional on



240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/google-checkout/cart.rb', line 240

def shipping_cost_xml
  xml = Builder::XmlMarkup.new
  if @flat_rate_shipping
    xml.price(:currency => currency) {
      xml.text! @flat_rate_shipping[:price].to_s
    }
  else
    xml.price(:currency => @currency) {
      xml.text! shipping_cost.to_s
    }
  end
end

#signatureObject

Returns the signature for the cart XML.



281
282
283
284
285
286
# File 'lib/google-checkout/cart.rb', line 281

def signature
  @xml or to_xml

  digest  = OpenSSL::Digest::Digest.new('sha1')
  OpenSSL::HMAC.digest(digest, @merchant_key, @xml)
end

#sizeObject

Number of items in the cart.



114
115
116
# File 'lib/google-checkout/cart.rb', line 114

def size
  @contents.size
end

#submit_domainObject



118
119
120
# File 'lib/google-checkout/cart.rb', line 118

def submit_domain
  (GoogleCheckout.production? ? 'checkout' : 'sandbox') + ".google.com"
end

#submit_urlObject

The Google Checkout form submission url.



125
126
127
# File 'lib/google-checkout/cart.rb', line 125

def submit_url
  GoogleCheckout.sandbox? ? (SANDBOX_CHECKOUT_URL % @merchant_id) : (PRODUCTION_CHECKOUT_URL % @merchant_id)
end

#to_xmlObject

This is the important method; it generatest the XML call. It’s fairly lengthy, but trivial. It follows the docs at code.google.com/apis/checkout/developer/index.html#checkout_api

It returns the raw XML string, not encoded.

Raises:

  • (RuntimeError)


165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/google-checkout/cart.rb', line 165

def to_xml
  raise RuntimeError, "Empty cart" if self.empty?

  xml = Builder::XmlMarkup.new
  xml.instruct!
  @xml = xml.tag!('checkout-shopping-cart', :xmlns => "http://checkout.google.com/schema/2") {
    xml.tag!("shopping-cart") {
      xml.items {
        @contents.each { |item|
          xml.item {
            if item.key?(:item_id)
              xml.tag!('merchant-item-id', item[:item_id])
            end
            xml.tag!('item-name') {
              xml.text! item[:name].to_s
            }
            xml.tag!('item-description') {
              xml.text! item[:description].to_s
            }
            xml.tag!('unit-price', :currency => (item[:currency] || 'USD')) {
              xml.text! item[:price].to_s
            }
            xml.quantity {
              xml.text! item[:quantity].to_s
            }
          }
        }
      }
      unless @merchant_private_data.empty?
        xml.tag!("merchant-private-data") {
          @merchant_private_data.each do |key, value|
            xml.tag!(key, value)
          end
        }
      end
    }
    xml.tag!('checkout-flow-support') {
      xml.tag!('merchant-checkout-flow-support') {
        xml.tag!('edit-cart-url', @edit_cart_url) if @edit_cart_url
        xml.tag!('continue-shopping-url', @continue_shopping_url) if @continue_shopping_url

        xml.tag!("request-buyer-phone-number", false)

        # TODO tax-tables
        xml.tag!("tax-tables") {
          xml.tag!("default-tax-table") {
            xml.tag!("tax-rules") {
              xml.tag!("default-tax-rule") {
                xml.tag!("shipping-taxed", false)
                xml.tag!("rate", "0.00")
                xml.tag!("tax-area") {
                  xml.tag!("world-area")
                }
              }
            }
          }
        }

        # TODO Shipping calculations
        #      These are currently hard-coded for PeepCode.
        #      Does anyone care to send a patch to enhance
        #      this for more flexibility?
        xml.tag!('shipping-methods') {
          xml.tag!('pickup', :name =>'Shipping') {
            xml.tag!('price', "2.00", :currency => currency)
          }
        }
      }
    }
  }
  @xml.dup
end