google4r 0.0.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 (39) hide show
  1. data/CHANGES +5 -0
  2. data/LICENSE +22 -0
  3. data/README +75 -0
  4. data/lib/google4r/checkout.rb +36 -0
  5. data/lib/google4r/checkout/commands.rb +267 -0
  6. data/lib/google4r/checkout/frontend.rb +100 -0
  7. data/lib/google4r/checkout/notifications.rb +533 -0
  8. data/lib/google4r/checkout/shared.rb +501 -0
  9. data/lib/google4r/checkout/xml_generation.rb +271 -0
  10. data/lib/google4r/maps.rb +174 -0
  11. data/test/checkout/integration/checkout_command_test.rb +103 -0
  12. data/test/checkout/unit/address_test.rb +131 -0
  13. data/test/checkout/unit/area_test.rb +41 -0
  14. data/test/checkout/unit/checkout_command_test.rb +112 -0
  15. data/test/checkout/unit/checkout_command_xml_generator_test.rb +187 -0
  16. data/test/checkout/unit/command_test.rb +126 -0
  17. data/test/checkout/unit/flat_rate_shipping_test.rb +114 -0
  18. data/test/checkout/unit/frontend_test.rb +63 -0
  19. data/test/checkout/unit/item_test.rb +159 -0
  20. data/test/checkout/unit/marketing_preferences_test.rb +65 -0
  21. data/test/checkout/unit/merchant_code_test.rb +122 -0
  22. data/test/checkout/unit/new_order_notification_test.rb +115 -0
  23. data/test/checkout/unit/notification_acknowledgement_test.rb +43 -0
  24. data/test/checkout/unit/notification_handler_test.rb +93 -0
  25. data/test/checkout/unit/order_adjustment_test.rb +95 -0
  26. data/test/checkout/unit/order_state_change_notification_test.rb +159 -0
  27. data/test/checkout/unit/pickup_shipping_test.rb +70 -0
  28. data/test/checkout/unit/private_data_parser_test.rb +68 -0
  29. data/test/checkout/unit/shipping_adjustment_test.rb +100 -0
  30. data/test/checkout/unit/shipping_method_test.rb +41 -0
  31. data/test/checkout/unit/shopping_cart_test.rb +146 -0
  32. data/test/checkout/unit/tax_rule_test.rb +65 -0
  33. data/test/checkout/unit/tax_table_test.rb +82 -0
  34. data/test/checkout/unit/us_country_area_test.rb +76 -0
  35. data/test/checkout/unit/us_state_area_test.rb +70 -0
  36. data/test/checkout/unit/us_zip_area_test.rb +66 -0
  37. data/test/maps/geocoder_test.rb +143 -0
  38. data/var/cacert.pem +7815 -0
  39. metadata +100 -0
@@ -0,0 +1,501 @@
1
+ #--
2
+ # Project: google4r
3
+ # File: lib/google4r/checkout/shared.rb
4
+ # Author: Manuel Holtgrewe <purestorm at ggnore dot net>
5
+ # Copyright: (c) 2007 by Manuel Holtgrewe
6
+ # License: MIT License as follows:
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining
9
+ # a copy of this software and associated documentation files (the
10
+ # "Software"), to deal in the Software without restriction, including
11
+ # without limitation the rights to use, copy, modify, merge, publish,
12
+ # distribute, sublicense, and/or sell copies of the Software, and to permit
13
+ # persons to whom the Software is furnished to do so, subject to the
14
+ # following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included
17
+ # in all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
22
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
23
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
24
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
25
+ # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ #++
27
+ # This file contains the classes and modules that are shared by the notification
28
+ # handling and parsing as well as the command generating code.
29
+
30
+ #--
31
+ # TODO
32
+ #
33
+ # * Make the optional attributes return defaults that make sense, i.e. Money.new(0)
34
+ # * Make the "main" API really pretty Ruby code and provide aliases so people can
35
+ # also just use the Google Docs.
36
+ #++
37
+ module Google4R #:nodoc:
38
+ module Checkout #:nodoc:
39
+ # This exception is thrown by Command#send_to_google_checkout when an error occured.
40
+ class GoogleCheckoutError < Exception
41
+ # The serial number of the error returned by Google.
42
+ attr_reader :serial_number
43
+
44
+ # The HTTP response code of Google's response.
45
+ attr_reader :response_code
46
+
47
+ # The parameter is a hash with the entries :serial_number, :message and :response_code.
48
+ # The attributes serial_number, message and response_code are set to the values in the
49
+ # Hash.
50
+ def initialize(hash)
51
+ @response_code = hash[:response_code]
52
+ @message = hash[:message]
53
+ @serial_number = hash[:serial_number]
54
+ end
55
+ end
56
+
57
+ # ShoppingCart instances are containers for Item instances. You can add
58
+ # Items to the class using #create_item (see the documentation of this
59
+ # method for an example).
60
+ class ShoppingCart
61
+ # The onwer of this cart. At the moment, this always is the CheckoutCartCommand.
62
+ attr_reader :owner
63
+
64
+ # The items in the cart. Do not modify this array directly but use
65
+ # #create_item to add items.
66
+ attr_reader :items
67
+
68
+ # You can set the <cart-expiration> time with this property. If left
69
+ # unset then the tag will not be generated and the cart will never
70
+ # expire.
71
+ attr_accessor :expires_at
72
+
73
+ # You can set almost arbitrary data into the cart using this method.
74
+ #
75
+ # The data will be converted to XML in the following way: The keys are converted
76
+ # to tag names (whitespace becomes "-", all chars not matching /[a-zA-Z0-9\-_])/
77
+ # will be removed.
78
+ #
79
+ # If a value is an array then the key for this value will be used as the tag
80
+ # name for each of the arrays's entries.
81
+ #
82
+ # Arrays will be flattened before it is processed.
83
+ #
84
+ # === Example
85
+ #
86
+ # cart.private_data = { 'foo' => { 'bar' => 'baz' } })
87
+ #
88
+ # # will produce the following XML
89
+ #
90
+ # <foo>
91
+ # <bar>baz</bar>
92
+ # </foo>
93
+ #
94
+ #
95
+ # cart.private_data = { 'foo' => [ { 'bar' => 'baz' }, "d'oh", 2 ] }
96
+ #
97
+ # # will produce the following XML
98
+ #
99
+ # <foo>
100
+ # <bar>baz</bar>
101
+ # </foo>
102
+ # <foo>d&amp;</foo>
103
+ # <foo>2</foo>
104
+ attr_reader :private_data
105
+
106
+ # Sets the value of the private_data attribute.
107
+ def private_data=(value)
108
+ raise "The given value #{value.inspect} is not a Hash!" unless value.kind_of?(Hash)
109
+ @private_data = value
110
+ end
111
+
112
+ # Initialize a new ShoppingCart with an empty Array for the items.
113
+ def initialize(owner)
114
+ @owner = owner
115
+ @items = Array.new
116
+ end
117
+
118
+ # Use this method to add a new item to the cart. If you use a block with
119
+ # this method then the block will be given the new item. The new item
120
+ # will be returned in any case.
121
+ #
122
+ # Passing a block is the preferred way of using this method.
123
+ #
124
+ # === Example
125
+ #
126
+ # # Using a block (preferred).
127
+ # cart = ShoppingCart.new
128
+ #
129
+ # cart.create_item do |item|
130
+ # item.name = "Dry Food Pack"
131
+ # item.description = "A pack of highly nutritious..."
132
+ # item.unit_price = Money.new(3500, "USD") # $35.00
133
+ # item.quantity = 1
134
+ # end
135
+ #
136
+ # # Not using a block.
137
+ # cart = ShoppingCart.new
138
+ #
139
+ # item = cart.create_item
140
+ # item.name = "Dry Food Pack"
141
+ # item.description = "A pack of highly nutritious..."
142
+ # item.unit_price = Money.new(3500, "USD") # $35.00
143
+ # item.quantity = 1
144
+ def create_item(&block)
145
+ item = Item.new(self)
146
+ @items << item
147
+
148
+ # Pass the newly generated item to the given block to set its attributes.
149
+ yield(item) if block_given?
150
+
151
+ return item
152
+ end
153
+
154
+ # Creates a new ShoppingCart object from a REXML::Element object.
155
+ def self.create_from_element(element, owner)
156
+ result = ShoppingCart.new(owner)
157
+
158
+ text = element.elements['cart-expiration/good-until-date'].text rescue nil
159
+ result.expires_at = Time.parse(text) unless text.nil?
160
+
161
+ data_element = element.elements['merchant-private-data']
162
+ value = PrivateDataParser.element_to_value(data_element) unless data_element.nil?
163
+
164
+ result.private_data = value unless value.nil?
165
+
166
+ element.elements.each('items/item') do |item_element|
167
+ result.items << Item.create_from_element(item_element, result)
168
+ end
169
+
170
+ return result
171
+ end
172
+ end
173
+
174
+ # An Item object represents a line of goods in the shopping cart/reciep.
175
+ #
176
+ # You should never initialize them directly but use ShoppingCart#create_item instead.
177
+ #
178
+ # Note that you have to create/set the tax tables for the owner of the cart in which
179
+ # the item is before you can set the tax_table attribute.
180
+ class Item
181
+ # The cart that this item belongs to.
182
+ attr_reader :cart
183
+
184
+ # The name of the cart item (string, required).
185
+ attr_accessor :name
186
+
187
+ # The description of the cart item (string, required).
188
+ attr_accessor :description
189
+
190
+ # The price for one unit of the given good (Money instance, required).
191
+ attr_reader :unit_price
192
+
193
+ # Sets the price for one unit of goods described by this item. money must respond to
194
+ # :cents and :currency as the Money class does.
195
+ def unit_price=(money)
196
+ if not (money.respond_to?(:cents) and money.respond_to?(:currency)) then
197
+ raise "Invalid price - does not respond to :cents and :currency - #{money.inspect}."
198
+ end
199
+
200
+ @unit_price = money
201
+ end
202
+
203
+ # Number of units that this item represents (integer, required).
204
+ attr_accessor :quantity
205
+
206
+ # Optional string value that is used to store the item's id (defined by the merchant)
207
+ # in the cart. Serialized to <merchant-item-id> in XML. Displayed by Google Checkout.
208
+ attr_accessor :id
209
+
210
+ # Optional hash value that is used to store the item's id (defined by the merchant)
211
+ # in the cart. Serialized to <merchant-private-item-data> in XML. Not displayed by
212
+ # Google Checkout.
213
+ #
214
+ # Must be a Hash. See ShoppingCart#private_data on how the serialization to XML is
215
+ # done.
216
+ attr_reader :private_data
217
+
218
+ # Sets the private data for this item.
219
+ def private_data=(value)
220
+ raise "The given value #{value.inspect} is not a Hash!" unless value.kind_of?(Hash)
221
+ @private_data = value
222
+ end
223
+
224
+ # The tax table to use for this item. Optional.
225
+ attr_reader :tax_table
226
+
227
+ # Sets the tax table to use for this item. When you set this attribute using this
228
+ # method then the used table must already be added to the cart. Otherwise, a
229
+ # RuntimeError will be raised.
230
+ def tax_table=(table)
231
+ raise "The table #{table.inspect} is not in the item's cart yet!" unless cart.owner.tax_tables.include?(table)
232
+
233
+ @tax_table = table
234
+ end
235
+
236
+ # Create a new Item in the given Cart. You should not instantize this class directly
237
+ # but use Cart#create_item instead.
238
+ def initialize(cart)
239
+ @cart = cart
240
+ end
241
+
242
+ # Creates a new Item object from a REXML::Element object.
243
+ def self.create_from_element(element, cart)
244
+ result = Item.new(cart)
245
+
246
+ result.name = element.elements['item-name'].text
247
+ result.description = element.elements['item-description'].text
248
+ result.quantity = element.elements['quantity'].text.to_i
249
+ result.id = element.elements['merchant-item-id'].text rescue nil
250
+
251
+ data_element = element.elements['merchant-private-item-data']
252
+ if not data_element.nil? then
253
+ value = PrivateDataParser.element_to_value(data_element)
254
+ result.private_data = value unless value.nil?
255
+ end
256
+
257
+ table_selector = element.elements['tax-table-selector'].text rescue nil
258
+ if not table_selector.nil? then
259
+ result.tax_table = cart.owner.tax_tables.find {|table| table.name == table_selector }
260
+ end
261
+
262
+ unit_price = (element.elements['unit-price'].text.to_f * 100).to_i
263
+ unit_price_currency = element.elements['unit-price/@currency'].value
264
+ result.unit_price = Money.new(unit_price, unit_price_currency)
265
+
266
+ return result
267
+ end
268
+ end
269
+
270
+ # A TaxTable is an ordered array of TaxRule objects. You should create the TaxRule
271
+ # instances using #create_rule
272
+ #
273
+ # You must set up a tax table factory and should only create tax tables from within
274
+ # its temporal factory method as described in the class documentation of Frontend.
275
+ #
276
+ # Each tax table must have one or more tax rules.
277
+ #
278
+ # === Example
279
+ #
280
+ # include Google4R::Checkout
281
+ #
282
+ # tax_free_table = TaxTable.new(false)
283
+ # tax_free_table.name = "default table"
284
+ # tax_free_table.create_rule do |rule|
285
+ # rule.area = UsCountryArea.new(UsCountryArea::ALL)
286
+ # rule.rate = 0.0
287
+ # end
288
+ class TaxTable
289
+ # The name of this tax table (string, required).
290
+ attr_accessor :name
291
+
292
+ # An Array of the TaxRule objects that this TaxTable contains. Use #create_rule do
293
+ # add to this Array but do not change it directly.
294
+ attr_reader :rules
295
+
296
+ # Boolean, true iff the table's standalone attribute is to be set to "true".
297
+ attr_reader :standalone
298
+
299
+ def initialize(standalone)
300
+ @rules = Array.new
301
+
302
+ @standalone = standalone
303
+ end
304
+
305
+ # Use this method to add a new TaxRule to the table. If you use a block with
306
+ # this method then the block will called with the newly created rule for the
307
+ # parameter. The method will return the new rule in any case.
308
+ def create_rule(&block)
309
+ rule = TaxRule.new(self)
310
+ @rules << rule
311
+
312
+ # Pass the newly generated rule to the given block to set its attributes.
313
+ yield(rule) if block_given?
314
+
315
+ return rule
316
+ end
317
+ end
318
+
319
+ # A TaxRule specifies which taxes to apply in which area. Have a look at the "Google
320
+ # Checkout documentation" [http://code.google.com/apis/checkout/developer/index.html#specifying_tax_info]
321
+ # for more information.
322
+ class TaxRule
323
+ # The table this rule belongs to.
324
+ attr_reader :table
325
+
326
+ # The tax rate for this rule (double, required).
327
+ attr_accessor :rate
328
+
329
+ # The area where this tax rule applies (Area subclass instance, required). Serialized
330
+ # to <tax-area> in XML.
331
+ attr_accessor :area
332
+
333
+ # Creates a new TaxRule in the given TaxTable. Do no call this method yourself
334
+ # but use TaxTable#create_rule instead!
335
+ def initialize(table)
336
+ @table = table
337
+ end
338
+ end
339
+
340
+ # Abstract class for areas that are used to specify a tax area. Do not use this class
341
+ # but only its subclasses.
342
+ class Area
343
+ # Mark this class as abstract by throwing a RuntimeError on initialization.
344
+ def initialize #:nodoc:
345
+ raise "Do not use the abstract class Google::Checkout::Area!"
346
+ end
347
+ end
348
+
349
+ # Instances of UsZipArea represent areas specified by US ZIPs and ZIP patterns.
350
+ class UsZipArea < Area
351
+ # The pattern for this ZIP area.
352
+ attr_accessor :pattern
353
+
354
+ # You can optionally initialize the Area with its value.
355
+ def initialize(pattern=nil)
356
+ self.pattern = pattern unless pattern.nil?
357
+ end
358
+ end
359
+
360
+ # Instances of UsStateArea represent states in the US.
361
+ class UsStateArea < Area
362
+ # The two-letter code of the US state.
363
+ attr_reader :state
364
+
365
+ # You can optionally initialize the Area with its value.
366
+ def initialize(state=nil)
367
+ self.state = state unless state.nil?
368
+ end
369
+
370
+ # Writer for the state attribute. value must match /^[A-Z]{2,2}$/.
371
+ def state=(value)
372
+ raise "Invalid US state: #{value}" unless value =~ /^[A-Z]{2,2}$/
373
+ @state = value
374
+ end
375
+ end
376
+
377
+ # Instances of UsCountryArea identify a region within the US.
378
+ class UsCountryArea < Area
379
+ CONTINENTAL_48 = "CONTINENTAL_48".freeze
380
+ FULL_50_STATES = "FULL_50_STATES".freeze
381
+ ALL = "ALL".freeze
382
+
383
+ # The area that is specified with this UsCountryArea (required). Can be
384
+ # one of UsCountryArea::CONTINENTAL_48, UsCountryArea::FULL_50_STATES
385
+ # and UsCountryArea::ALL.
386
+ # See the Google Checkout API for information on these values.
387
+ attr_reader :area
388
+
389
+ # You can optionally initialize the Area with its value.
390
+ def initialize(area=nil)
391
+ self.area = area unless area.nil?
392
+ end
393
+
394
+ # Writer for the area attribute. value must be one of CONTINENTAL_48,
395
+ # FULL_50_STATES and ALL
396
+ def area=(value)
397
+ raise "Invalid area :#{value}!" unless [ CONTINENTAL_48, FULL_50_STATES, ALL ].include?(value)
398
+ @area = value
399
+ end
400
+ end
401
+
402
+ # Abstract class for shipping methods. Do not use this class directly but only
403
+ # one of its subclasses.
404
+ class ShippingMethod
405
+ # The name of the shipping method (string, required).
406
+ attr_accessor :name
407
+
408
+ # The price of the shipping method (Money instance, required).
409
+ attr_reader :price
410
+
411
+ # Sets the cost for this shipping method. money must respond to :cents and :currency
412
+ # as Money objects would.
413
+ def price=(money)
414
+ if not (money.respond_to?(:cents) and money.respond_to?(:currency)) then
415
+ raise "Invalid cost - does not respond to :cents and :currency - #{money.inspect}."
416
+ end
417
+
418
+ @price = money
419
+ end
420
+
421
+ # Mark this class as abstract by throwing a RuntimeError on initialization.
422
+ def initialize
423
+ raise "Do not use the abstract class Google::Checkout::ShippingMethod!"
424
+ end
425
+ end
426
+
427
+ # A class that represents the "pickup" shipping method.
428
+ class PickupShipping < ShippingMethod
429
+ def initialize
430
+ end
431
+ end
432
+
433
+ # A class that represents the "flat_rate" shipping method.
434
+ class FlatRateShipping < ShippingMethod
435
+ # An Array of allowed areas for this flat_rate shipping instance. Use
436
+ # #create_allowed_area to add to this area but do not change it directly.
437
+ attr_reader :allowed_areas
438
+
439
+ # An Array of excluded areas for this flat_rate shipping instance. Use
440
+ # #create_excluded_area to add to this area but do not change it directly.
441
+ attr_reader :excluded_areas
442
+
443
+ def initialize
444
+ @allowed_areas = Array.new
445
+ @excluded_areas = Array.new
446
+ end
447
+
448
+ # Creates a new Area, adds it to the internal list of allowed areas for this
449
+ # shipping types. If you passed a block (preferred) then the block is called
450
+ # with the Area as the only parameter.c
451
+ #
452
+ # The area to be created depends on the given parameter clazz. It can be one
453
+ # of { UsCountryArea, UsStateArea, UsZipArea }.
454
+ #
455
+ # Raises a RuntimeError if the parameter clazz is invalid.
456
+ #
457
+ # === Example
458
+ #
459
+ # method = FlatRateShipping.new
460
+ # method.create_allowed_area(UsCountryArea) do |area|
461
+ # area.area = UsCountryArea::ALL
462
+ # end
463
+ def create_allowed_area(clazz, &block)
464
+ raise "Invalid Area class: #{clazz}!" unless [ UsCountryArea, UsStateArea, UsZipArea ].include?(clazz)
465
+
466
+ area = clazz.new
467
+ @allowed_areas << area
468
+
469
+ yield(area) if block_given?
470
+
471
+ return area
472
+ end
473
+
474
+ # Creates a new Area, adds it to the internal list of excluded areas for this
475
+ # shipping types. If you passed a block (preferred) then the block is called
476
+ # with the Area as the only parameter. The created area is returned in any case.
477
+ #
478
+ # The area to be created depends on the given parameter clazz. It can be one
479
+ # of { UsCountryArea, UsStateArea, UsZipArea }.
480
+ #
481
+ # Raises a RuntimeError if the parameter clazz is invalid.
482
+ #
483
+ # === Example
484
+ #
485
+ # method = FlatRateShipping.new
486
+ # method.create_excluded_area(UsCountryArea) do |area|
487
+ # area.area = UsCountryArea::ALL
488
+ # end
489
+ def create_excluded_area(clazz, &block)
490
+ raise "Invalid Area class: #{clazz}!" unless [ UsCountryArea, UsStateArea, UsZipArea ].include?(clazz)
491
+
492
+ area = clazz.new
493
+ @excluded_areas << area
494
+
495
+ yield(area) if block_given?
496
+
497
+ return area
498
+ end
499
+ end
500
+ end
501
+ end