google4r 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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