google4r-checkout 0.1.0

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