nbudin-google4r-checkout 1.0.6

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 (79) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGES +83 -0
  3. data/LICENSE +22 -0
  4. data/README +58 -0
  5. data/Rakefile +80 -0
  6. data/VERSION +1 -0
  7. data/lib/google4r/checkout/commands.rb +515 -0
  8. data/lib/google4r/checkout/frontend.rb +204 -0
  9. data/lib/google4r/checkout/merchant_calculation.rb +321 -0
  10. data/lib/google4r/checkout/notifications.rb +708 -0
  11. data/lib/google4r/checkout/shared.rb +1249 -0
  12. data/lib/google4r/checkout/utils.rb +94 -0
  13. data/lib/google4r/checkout/xml_generation.rb +920 -0
  14. data/lib/google4r/checkout.rb +34 -0
  15. data/test/frontend_configuration.rb +13 -0
  16. data/test/integration/checkout_command_test.rb +246 -0
  17. data/test/test_helper.rb +115 -0
  18. data/test/unit/add_merchant_order_number_command_test.rb +70 -0
  19. data/test/unit/add_tracking_data_command_test.rb +75 -0
  20. data/test/unit/address_test.rb +131 -0
  21. data/test/unit/anonymous_address_test.rb +75 -0
  22. data/test/unit/archive_order_command_test.rb +66 -0
  23. data/test/unit/area_test.rb +44 -0
  24. data/test/unit/authorization_amount_notification_test.rb +69 -0
  25. data/test/unit/authorize_order_command_test.rb +66 -0
  26. data/test/unit/backorder_items_command_test.rb +83 -0
  27. data/test/unit/callback_handler_test.rb +83 -0
  28. data/test/unit/cancel_items_command_test.rb +89 -0
  29. data/test/unit/cancel_order_command_test.rb +83 -0
  30. data/test/unit/carrier_calculated_shipping_test.rb +57 -0
  31. data/test/unit/charge_amount_notification_test.rb +64 -0
  32. data/test/unit/charge_order_command_test.rb +77 -0
  33. data/test/unit/chargeback_amount_notification_test.rb +65 -0
  34. data/test/unit/checkout_command_test.rb +125 -0
  35. data/test/unit/checkout_command_xml_generator_test.rb +218 -0
  36. data/test/unit/command_test.rb +116 -0
  37. data/test/unit/deliver_order_command_test.rb +70 -0
  38. data/test/unit/delivery_method_test.rb +42 -0
  39. data/test/unit/digital_content_test.rb +105 -0
  40. data/test/unit/flat_rate_shipping_test.rb +132 -0
  41. data/test/unit/frontend_test.rb +136 -0
  42. data/test/unit/item_info_test.rb +69 -0
  43. data/test/unit/item_test.rb +171 -0
  44. data/test/unit/marketing_preferences_test.rb +65 -0
  45. data/test/unit/merchant_calculated_shipping_test.rb +172 -0
  46. data/test/unit/merchant_calculation_callback_test.rb +137 -0
  47. data/test/unit/merchant_calculation_result_test.rb +78 -0
  48. data/test/unit/merchant_calculation_results_test.rb +178 -0
  49. data/test/unit/merchant_code_result_test.rb +51 -0
  50. data/test/unit/merchant_code_test.rb +122 -0
  51. data/test/unit/new_order_notification_test.rb +115 -0
  52. data/test/unit/notification_acknowledgement_test.rb +67 -0
  53. data/test/unit/notification_handler_test.rb +113 -0
  54. data/test/unit/order_adjustment_test.rb +119 -0
  55. data/test/unit/order_state_change_notification_test.rb +158 -0
  56. data/test/unit/pickup_shipping_test.rb +70 -0
  57. data/test/unit/postal_area_test.rb +71 -0
  58. data/test/unit/private_data_parser_test.rb +68 -0
  59. data/test/unit/refund_amount_notification_test.rb +65 -0
  60. data/test/unit/refund_order_command_test.rb +86 -0
  61. data/test/unit/reset_items_shipping_information_command_test.rb +83 -0
  62. data/test/unit/return_items_command_test.rb +83 -0
  63. data/test/unit/risk_information_notification_test.rb +98 -0
  64. data/test/unit/send_buyer_message_command_test.rb +73 -0
  65. data/test/unit/ship_items_command_test.rb +101 -0
  66. data/test/unit/shipping_adjustment_test.rb +100 -0
  67. data/test/unit/shopping_cart_test.rb +146 -0
  68. data/test/unit/tax_rule_test.rb +70 -0
  69. data/test/unit/tax_table_test.rb +82 -0
  70. data/test/unit/tracking_data_test.rb +54 -0
  71. data/test/unit/unarchive_order_command_test.rb +66 -0
  72. data/test/unit/us_country_area_test.rb +76 -0
  73. data/test/unit/us_state_area_test.rb +70 -0
  74. data/test/unit/us_zip_area_test.rb +66 -0
  75. data/test/unit/world_area_test.rb +48 -0
  76. data/test/xml/apiv2.xsd +997 -0
  77. data/test/xml/test_check_persisting_works_expected.xml +213 -0
  78. data/var/cacert.pem +7815 -0
  79. metadata +200 -0
@@ -0,0 +1,708 @@
1
+ #--
2
+ # Project: google4r
3
+ # File: lib/google4r/checkout/notifications.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 used in the notification
28
+ # handling code.
29
+
30
+ require 'rexml/document'
31
+
32
+ module Google4R #:nodoc:
33
+ module Checkout #:nodoc:
34
+ # Thrown by Notification on unimplemented and unknown notification from Google.
35
+ class UnknownNotificationType < Exception
36
+ end
37
+
38
+ # Represents a notification acknowledgment to tell Google that the
39
+ # notification has been recieved and processed. Google guarantees not to
40
+ # resend any notification it has recieved an acknowledgment for.
41
+ #
42
+ # === For example, in a Rails app
43
+ #
44
+ # # Let Google Checkout know we handled this notification with handshake
45
+ # notification_acknowledgement = Google4R::Checkout::NotificationAcknowledgement.new(notification).to_xml
46
+ # render :text => notification_acknowledgement, :status => 200
47
+ #
48
+ # or
49
+ #
50
+ # # Without handshake
51
+ # notification_acknowledgement = Google4R::Checkout::NotificationAcknowledgement.new.to_xml
52
+ # render :text => notification_acknowledgement, :status => 200
53
+ #
54
+ class NotificationAcknowledgement
55
+
56
+ attr_reader :serial_number
57
+
58
+ def initialize(notification=nil)
59
+ @serial_number = notification.serial_number unless notification.nil?
60
+ end
61
+
62
+ def to_xml
63
+ NotificationAcknowledgementXmlGenerator.new(self).generate
64
+ end
65
+ end
66
+
67
+ # This class expects the message sent by Google. It parses the XMl document and returns
68
+ # the appropriate Notification. If the notification sent by Google is invalid then a
69
+ # UnknownNotificationType is raised that you should catch and then send a 404 to Google
70
+ # to indicate that the notification handler has not been implemented yet.
71
+ #
72
+ # See http://code.google.com/apis/checkout/developer/index.html#notification_api for
73
+ # details.
74
+ #
75
+ # Note that you must protect the HTTPS request to the piece of code using a
76
+ # NotificationHandler by HTTP Auth Basic. If you are using Ruby On Rails then you can
77
+ # use the great "simple_http_auth" plugin you can find here:
78
+ # http://blog.codahale.com/2006/05/11/basic-http-authentication-with-rails-simple_http_auth/
79
+ #
80
+ # === Usage Example
81
+ #
82
+ # When you use a Rails controller to handle the calbacks by Google then your action to handle
83
+ # the callbacks could use a NotificationHandler as follows:
84
+ #
85
+ # def google_checkout_api
86
+ # frontend = Google4R::Checkout::Frontend.new(FRONTEND_CONFIGURATION)
87
+ # frontend.tax_table_factory = CheckoutCommandFactory.new
88
+ # handler = frontend.create_notification_handler
89
+ #
90
+ # begin
91
+ # notification = handler.handle(request.raw_post) # raw_post contains the XML
92
+ # rescue Google4R::Checkout::UnknownNotificationType => e
93
+ # # This can happen if Google adds new commands and Google4R has not been
94
+ # # upgraded yet. It is not fatal.
95
+ # render :text => 'ignoring unknown notification type', :status => 200
96
+ # return
97
+ # end
98
+ #
99
+ # # ...
100
+ # end
101
+ class NotificationHandler
102
+ # The Frontend object that created this NotificationHandler
103
+ attr_accessor :frontend
104
+
105
+ # Create a new NotificationHandler and assign value of the parameter frontend to
106
+ # the frontend attribute.
107
+ def initialize(frontend)
108
+ @frontend = frontend
109
+ end
110
+
111
+ # Parses the given xml_str and returns the appropriate *Notification class. At the
112
+ # moment, only NewOrderNotification and OrderStateChangeNotification objects can be
113
+ # returned.
114
+ #--
115
+ # TODO: Add parsing of other notifications here (merchant calculation and the like) when they have been implemented.
116
+ #++
117
+ def handle(xml_str)
118
+ root = REXML::Document.new(xml_str).root
119
+
120
+ case root.name
121
+ when 'new-order-notification' then
122
+ NewOrderNotification.create_from_element(root, frontend)
123
+ when 'order-state-change-notification' then
124
+ OrderStateChangeNotification.create_from_element(root, frontend)
125
+ when 'risk-information-notification' then
126
+ RiskInformationNotification.create_from_element(root, frontend)
127
+ when 'charge-amount-notification' then
128
+ ChargeAmountNotification.create_from_element(root, frontend)
129
+ when 'refund-amount-notification' then
130
+ RefundAmountNotification.create_from_element(root, frontend)
131
+ when 'chargeback-amount-notification' then
132
+ ChargebackAmountNotification.create_from_element(root, frontend)
133
+ when 'authorization-amount-notification' then
134
+ AuthorizationAmountNotification.create_from_element(root, frontend)
135
+ else
136
+ raise UnknownNotificationType, "Unknown notification type: #{root.name}"
137
+ end
138
+ end
139
+ end
140
+
141
+ # Abstract class for all the notifications. It should not be instantiated
142
+ # directly.
143
+ class Notification
144
+ # The frontend this notification belongs to.
145
+ attr_accessor :frontend
146
+
147
+ # The serial number of the new order notification (String).
148
+ attr_accessor :serial_number
149
+
150
+ # The Google order number the new order notification belongs to (String).
151
+ attr_accessor :google_order_number
152
+
153
+ # The timestamp of the notification. Also see #timestamp=
154
+ attr_accessor :timestamp
155
+
156
+ # Initializes the RiskInformationNotification instance with the given Frontend instance.
157
+ def initialize(frontend)
158
+ @frontend = frontend
159
+ end
160
+
161
+ end
162
+
163
+ # Google Checkout sends <new-order-notification> messages to the web service when a new
164
+ # order has been successfully filed with Google Checkout. These messages will be parsed
165
+ # into NewOrderNotification instances.
166
+ class NewOrderNotification < Notification
167
+
168
+ # The buyer's billing address (Address).
169
+ attr_accessor :buyer_billing_address
170
+
171
+ # The buyer's shipping adress (Address).
172
+ attr_accessor :buyer_shipping_address
173
+
174
+ # The buyer's ID from Google Checkout (String).
175
+ attr_accessor :buyer_id
176
+
177
+ # The buyer's marketing preferences (MarketingPreferences).
178
+ attr_accessor :marketing_preferences
179
+
180
+ # The order's financial order state (String, one of FinancialOrderState::*).
181
+ attr_accessor :financial_order_state
182
+
183
+ # The order's fulfillment state (String, one of FulfillmentOrderState::*).
184
+ attr_accessor :fulfillment_order_state
185
+
186
+ # The order's number at Google Checkout (String).
187
+ attr_accessor :google_order_number
188
+
189
+ # The order's total adjustment (OrderAdjustment).
190
+ attr_accessor :order_adjustment
191
+
192
+ # The order's total amount (Money).
193
+ attr_accessor :order_total
194
+
195
+ # The order's shopping cart (ShoppingCart)
196
+ attr_accessor :shopping_cart
197
+
198
+ # The tax tables for the items in the order notification.
199
+ attr_reader :tax_tables
200
+
201
+ # Set the order's timestamp (Time). When the timestamp is set then the tax tables valid
202
+ # at the given point of time are set into the attribute tax tables from the frontend's
203
+ # tax_table_factory.
204
+ def timestamp=(time)
205
+ @timestamp = time
206
+ @tax_tables = frontend.tax_table_factory.effective_tax_tables_at(time)
207
+ end
208
+
209
+ # Factory method to create a new CheckoutNotification object from the REXML:Element object
210
+ #
211
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
212
+ # elements.
213
+ #
214
+ # You have to pass in the Frontend class this notification belongs to.
215
+ def self.create_from_element(element, frontend)
216
+ result = NewOrderNotification.new(frontend)
217
+
218
+ result.timestamp = Time.parse(element.elements['timestamp'].text)
219
+ result.serial_number = element.attributes['serial-number']
220
+ result.google_order_number = element.elements['google-order-number'].text
221
+ result.buyer_billing_address = Address.create_from_element(element.elements['buyer-billing-address'])
222
+ result.buyer_shipping_address = Address.create_from_element(element.elements['buyer-shipping-address'])
223
+ result.buyer_id = element.elements['buyer-id'].text
224
+ result.marketing_preferences = MarketingPreferences.create_from_element(element.elements['buyer-marketing-preferences'])
225
+ result.financial_order_state = element.elements['financial-order-state'].text
226
+ result.fulfillment_order_state = element.elements['fulfillment-order-state'].text
227
+ result.order_adjustment = OrderAdjustment.create_from_element(element.elements['order-adjustment'])
228
+ result.shopping_cart = ShoppingCart.create_from_element(element.elements['shopping-cart'], result)
229
+
230
+ amount = (element.elements['order-total'].text.to_f*100).to_i
231
+ currency = element.elements['order-total'].attributes['currency']
232
+ result.order_total = Money.new(amount, currency)
233
+
234
+ return result
235
+ end
236
+ end
237
+
238
+ # GoogleCheckout sends <order-change-notification> messages to the web service when the
239
+ # order's state changes. They will get parsed into OrderStateChangeNotification objects.
240
+ class OrderStateChangeNotification < Notification
241
+
242
+ # The previous financial state of the order (String, one of FinancialOrderState::*).
243
+ attr_accessor :previous_financial_order_state
244
+
245
+ # The new financial state of the order (String, one of FinancialOrderState::*).
246
+ attr_accessor :new_financial_order_state
247
+
248
+ # The previous fulfillment state of the order (String, one of FulfillmentOrderState::*).
249
+ attr_accessor :previous_fulfillment_order_state
250
+
251
+ # The new fulfillment state of the order (String, one of FulfillmentOrderState::*).
252
+ attr_accessor :new_fulfillment_order_state
253
+
254
+ # The reason for the change (String, can be nil).
255
+ attr_accessor :reason
256
+
257
+ # The tax tables for the items in the order notification.
258
+ attr_reader :tax_tables
259
+
260
+ # Set the order's timestamp (Time). When the timestamp is set then the tax tables valid
261
+ # at the given point of time are set into the attribute tax tables from the frontend's
262
+ # tax_table_factory.
263
+ def timestamp=(time)
264
+ @timestamp = time
265
+ @tax_tables = frontend.tax_table_factory.effective_tax_tables_at(time)
266
+ end
267
+
268
+ # Factory method that creates a new OrderStateChangeNotification from an REXML::Element instance.
269
+ # Use this to create instances of OrderStateChangeNotification.
270
+ #
271
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
272
+ # elements.
273
+ def self.create_from_element(element, frontend)
274
+ result = OrderStateChangeNotification.new(frontend)
275
+
276
+ result.timestamp = Time.parse(element.elements['timestamp'].text)
277
+ result.serial_number = element.attributes['serial-number']
278
+ result.google_order_number = element.elements['google-order-number'].text
279
+ result.new_financial_order_state = element.elements['new-financial-order-state'].text
280
+ result.previous_financial_order_state = element.elements['previous-financial-order-state'].text
281
+ result.new_fulfillment_order_state = element.elements['new-fulfillment-order-state'].text
282
+ result.previous_fulfillment_order_state = element.elements['previous-fulfillment-order-state'].text
283
+ result.reason = element.elements['reason'].text rescue nil
284
+
285
+ return result
286
+ end
287
+ end
288
+
289
+ # Google Checkout sends <charge-amount-notification> messages to the web service when the
290
+ # to confirm that the charge was successfully executed.
291
+ class ChargeAmountNotification < Notification
292
+
293
+ # The amount most recently charged for an order (Money)
294
+ attr_accessor :latest_charge_amount
295
+
296
+ # The total amount charged for an order (Money)
297
+ attr_accessor :total_charge_amount
298
+
299
+ # Factory method that creates a new ChargeAmountNotification from an REXML::Element instance.
300
+ # Use this to create instances of ChargeAmountNotification.
301
+ #
302
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
303
+ # elements.
304
+ def self.create_from_element(element, frontend)
305
+ charge = ChargeAmountNotification.new(frontend)
306
+
307
+ charge.serial_number = element.attributes['serial-number']
308
+ charge.google_order_number = element.elements['google-order-number'].text
309
+
310
+ currency = element.elements['latest-charge-amount'].attributes['currency']
311
+ amount = (element.elements['latest-charge-amount'].text.to_f*100).to_i
312
+ charge.latest_charge_amount = Money.new(amount, currency)
313
+
314
+ currency = element.elements['total-charge-amount'].attributes['currency']
315
+ amount = (element.elements['total-charge-amount'].text.to_f*100).to_i
316
+ charge.total_charge_amount = Money.new(amount, currency)
317
+
318
+ charge.timestamp = Time.parse(element.elements['timestamp'].text)
319
+
320
+ return charge
321
+ end
322
+ end
323
+
324
+ # Google Checkout sends a <refund-amount-notification> after successfully executing
325
+ # a <refund-order> order processing command. See the Google Checkout documentation for more details:
326
+ # http://code.google.com/apis/checkout/developer/index.html#refund_amount_notification
327
+ class RefundAmountNotification < Notification
328
+
329
+ # The amount most recently refunded for an order (Money)
330
+ attr_accessor :latest_refund_amount
331
+
332
+ # The total amount refunded for an order (Money)
333
+ attr_accessor :total_refund_amount
334
+
335
+ # Factory method that creates a new RefundAmountNotification from an REXML::Element instance.
336
+ # Use this to create instances of RefundAmountNotification.
337
+ #
338
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
339
+ # elements.
340
+ def self.create_from_element(element, frontend)
341
+ refund = RefundAmountNotification.new(frontend)
342
+
343
+ refund.serial_number = element.attributes['serial-number']
344
+ refund.google_order_number = element.elements['google-order-number'].text
345
+
346
+ currency = element.elements['latest-refund-amount'].attributes['currency']
347
+ amount = (element.elements['latest-refund-amount'].text.to_f*100).to_i
348
+ refund.latest_refund_amount = Money.new(amount, currency)
349
+
350
+ currency = element.elements['total-refund-amount'].attributes['currency']
351
+ amount = (element.elements['total-refund-amount'].text.to_f*100).to_i
352
+ refund.total_refund_amount = Money.new(amount, currency)
353
+
354
+ refund.timestamp = Time.parse(element.elements['timestamp'].text)
355
+
356
+ return refund
357
+ end
358
+ end
359
+
360
+ # Google Checkout sends a <chargeback-amount-notification> when a customer initiates a
361
+ # chargeback against the order and Google approves the chargeback.
362
+ # See the Google Checkout documentation for more details:
363
+ # http://code.google.com/apis/checkout/developer/index.html#chargeback_amount_notification
364
+ class ChargebackAmountNotification < Notification
365
+
366
+ # The amount most recently charged back for an order (Money)
367
+ attr_accessor :latest_chargeback_amount
368
+
369
+ # The total amount charged back for an order (Money)
370
+ attr_accessor :total_chargeback_amount
371
+
372
+ # Factory method that creates a new ChargebackAmountNotification from an REXML::Element instance.
373
+ # Use this to create instances of ChargebackAmountNotification.
374
+ #
375
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
376
+ # elements.
377
+ def self.create_from_element(element, frontend)
378
+ chargeback = ChargebackAmountNotification.new(frontend)
379
+
380
+ chargeback.serial_number = element.attributes['serial-number']
381
+ chargeback.google_order_number = element.elements['google-order-number'].text
382
+
383
+ currency = element.elements['latest-chargeback-amount'].attributes['currency']
384
+ amount = (element.elements['latest-chargeback-amount'].text.to_f*100).to_i
385
+ chargeback.latest_chargeback_amount = Money.new(amount, currency)
386
+
387
+ currency = element.elements['total-chargeback-amount'].attributes['currency']
388
+ amount = (element.elements['total-chargeback-amount'].text.to_f*100).to_i
389
+ chargeback.total_chargeback_amount = Money.new(amount, currency)
390
+
391
+ chargeback.timestamp = Time.parse(element.elements['timestamp'].text)
392
+
393
+ return chargeback
394
+ end
395
+ end
396
+
397
+ # Google Checkout sends an <authorization-amount-notification> in response to a successful
398
+ # request for an explicit credit card reauthorization.
399
+ # See the Google Checkout documentation for more details:
400
+ # http://code.google.com/apis/checkout/developer/index.html#authorization_amount_notification
401
+ class AuthorizationAmountNotification < Notification
402
+
403
+ # The amount that is reauthorized to be charged to the customer's credit card (Money)
404
+ attr_accessor :authorization_amount
405
+
406
+ # The time that a credit card authorization for an order expires.
407
+ attr_accessor :authorization_expiration_date
408
+
409
+ # The address verification response (String)
410
+ attr_accessor :avs_response
411
+
412
+ # Credit verification value for the order (String)
413
+ attr_accessor :cvn_response
414
+
415
+ # Factory method that creates a new AuthorizationAmountNotification from an REXML::Element instance.
416
+ # Use this to create instances of AuthorizationAmountNotification.
417
+ #
418
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
419
+ # elements.
420
+ def self.create_from_element(element, frontend)
421
+ authorization = AuthorizationAmountNotification.new(frontend)
422
+
423
+ authorization.serial_number = element.attributes['serial-number']
424
+ authorization.google_order_number = element.elements['google-order-number'].text
425
+
426
+ currency = element.elements['authorization-amount'].attributes['currency']
427
+ amount = (element.elements['authorization-amount'].text.to_f*100).to_i
428
+ authorization.authorization_amount = Money.new(amount, currency)
429
+
430
+ authorization.authorization_expiration_date = Time.parse(element.elements['authorization-expiration-date'].text)
431
+
432
+ authorization.avs_response = element.elements['avs-response'].text
433
+ authorization.cvn_response = element.elements['cvn-response'].text
434
+
435
+ authorization.timestamp = Time.parse(element.elements['timestamp'].text)
436
+
437
+ return authorization
438
+ end
439
+ end
440
+
441
+ # Google Checkout sends out <risk-information-notification> messages for fraud detection
442
+ # related information. See the Google Checkout documentation for more details:
443
+ # http://code.google.com/apis/checkout/developer/index.html#risk_information_notification
444
+ class RiskInformationNotification < Notification
445
+
446
+ # Is the order eligible for Google Checkout's payment guarantee policy (boolean).
447
+ attr_accessor :eligible_for_protection
448
+
449
+ # The buyer's billing address (Address).
450
+ attr_accessor :buyer_billing_address
451
+
452
+ # The address verification response (String)
453
+ attr_accessor :avs_response
454
+
455
+ # Credit verification value for the order (String)
456
+ attr_accessor :cvn_response
457
+
458
+ # The last 4 digits of the credit card used to pay for the order (integer)
459
+ attr_accessor :partial_card_number
460
+
461
+ # The buyer's IP address (String)
462
+ attr_accessor :ip_address
463
+
464
+ # The age of the buyer's google checkout account in days
465
+ attr_accessor :buyer_account_age
466
+
467
+ # Factory method that creates a new RiskInformationNotification from an REXML::Element instance.
468
+ # Use this to create instances of RiskInformationNotification
469
+ #
470
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
471
+ # elements.
472
+ def self.create_from_element(element, frontend)
473
+ risk = RiskInformationNotification.new(frontend)
474
+
475
+ risk.serial_number = element.attributes['serial-number']
476
+ risk.timestamp = Time.parse(element.elements['timestamp'].text)
477
+ risk.partial_card_number = element.elements['risk-information/partial-cc-number'].text
478
+ risk.ip_address = element.elements['risk-information/ip-address'].text
479
+ risk.google_order_number = element.elements['google-order-number'].text
480
+ risk.eligible_for_protection = element.elements['risk-information/eligible-for-protection'].text == 'true'
481
+ risk.cvn_response = element.elements['risk-information/cvn-response'].text
482
+ risk.buyer_billing_address = Address.create_from_element(element.elements['risk-information/billing-address'])
483
+ risk.buyer_account_age = element.elements['risk-information/buyer-account-age'].text.to_i
484
+ risk.avs_response = element.elements['risk-information/avs-response'].text
485
+
486
+ return risk
487
+ end
488
+ end
489
+
490
+ # Container for the valid financial order states as defined in the
491
+ # Google Checkout API.
492
+ module FinancialOrderState
493
+ REVIEWING = "REVIEWING".freeze
494
+ CHARGEABLE = "CHARGEABLE".freeze
495
+ CHARGING = "CHARGING".freeze
496
+ CHARGED = "CHARGED".freeze
497
+ PAYMENT_DECLINED = "PAYMENT_DECLINED".freeze
498
+ CANCELLED = "CANCELLED".freeze
499
+ CANCELLED_BY_GOOGLE = "CANCELLED_BY_GOOGLE".freeze
500
+ end
501
+
502
+ # Container for the valid fulfillment order states as defined in the
503
+ # Google Checkout API.
504
+ module FulfillmentOrderState
505
+ NEW = "NEW".freeze
506
+ PROCESSING = "PROCESSING".freeze
507
+ DELIVERED = "DELIVERED".freeze
508
+ WILL_NOT_DELIVER = "WILL_NOT_DELIVER".freeze
509
+ end
510
+
511
+ # The marketing preferences of a customer.
512
+ class MarketingPreferences
513
+ # Boolean, true if the customer wants to receive emails.
514
+ attr_accessor :email_allowed
515
+
516
+ # Creates a new MarketingPreferences object from a given REXML::Element instance.
517
+ def self.create_from_element(element)
518
+ result = MarketingPreferences.new
519
+
520
+ result.email_allowed = (element.elements['email-allowed'].text.downcase == 'true')
521
+
522
+ return result
523
+ end
524
+ end
525
+
526
+ # MerchantCodes represent gift certificates or coupons that have been used in an order.
527
+ #
528
+ # Only used with Merchant Calculations.
529
+ class MerchantCode
530
+ GIFT_CERTIFICATE = "GIFT_CERTIFICATE".freeze
531
+ COUPON = "COUPON".freeze
532
+
533
+ # The type of the adjustment. Can be one of GIFT_CERTIFICATE and COUPON.
534
+ attr_accessor :type
535
+
536
+ # The adjustment's code (String).
537
+ attr_accessor :code
538
+
539
+ # The amount of money that has been calculated as the adjustment's worth (Money, optional).
540
+ attr_accessor :calculated_amount
541
+
542
+ # The amount of the adjustment that has been applied to the cart's total (Money).
543
+ attr_accessor :applied_amount
544
+
545
+ # The message associated with the direct adjustment (String, optional).
546
+ attr_accessor :message
547
+
548
+ # Creates the MerchantCode from the given REXML::Element instance. The Element's
549
+ # name must be "gift-certificate-adjustment" or "coupon-adjustment".
550
+ def self.create_from_element(element)
551
+ result = MerchantCode.new
552
+
553
+ result.type =
554
+ case element.name
555
+ when 'gift-certificate-adjustment' then
556
+ GIFT_CERTIFICATE
557
+ when 'coupon-adjustment' then
558
+ COUPON
559
+ else
560
+ raise ArgumentError, "Invalid tag name: #{element.name} in \n—-\n#{element.to_s}\n—-."
561
+ end
562
+
563
+ result.code = element.elements['code'].text
564
+
565
+ amount = (element.elements['calculated-amount'].text.to_f*100).to_i rescue nil
566
+ currency = element.elements['calculated-amount'].attributes['currency'] rescue nil
567
+ result.calculated_amount = Money.new(amount, currency) unless amount.nil?
568
+
569
+ amount = (element.elements['applied-amount'].text.to_f*100).to_i
570
+ currency = element.elements['applied-amount'].attributes['currency']
571
+ result.applied_amount = Money.new(amount, currency)
572
+
573
+ result.message = element.elements['message'].text rescue nil
574
+
575
+ return result
576
+ end
577
+ end
578
+
579
+ # ShippingAdjustments represent the chosen shipping method.
580
+ class ShippingAdjustment
581
+ MERCHANT_CALCULATED = "MERCHANT_CALCULATED".freeze
582
+ FLAT_RATE = "FLAT_RATE".freeze
583
+ PICKUP = "PICKUP".freeze
584
+
585
+ # The type of the shipping adjustment, one of MERCHANT_CALCULATED, FLAT_RATE
586
+ # PICKUP.
587
+ attr_accessor :type
588
+
589
+ # The name of the shipping adjustment.
590
+ attr_accessor :name
591
+
592
+ # The cost of the selected shipping (Money).
593
+ attr_accessor :cost
594
+
595
+ # Creates a new ShippingAdjustment object from a REXML::Element object.
596
+ #
597
+ # Can raise a RuntimeException if the given Element is invalid.
598
+ def self.create_from_element(element)
599
+ result = ShippingAdjustment.new
600
+
601
+ result.type =
602
+ case element.name
603
+ when 'flat-rate-shipping-adjustment' then
604
+ FLAT_RATE
605
+ when 'pickup-shipping-adjustment' then
606
+ PICKUP
607
+ when 'merchant-calculated-shipping-adjustment' then
608
+ MERCHANT_CALCULATED
609
+ else
610
+ raise "Unexpected shipping adjustment '#{element.name}'"
611
+ end
612
+
613
+ result.name = element.elements['shipping-name'].text
614
+
615
+ amount = (element.elements['shipping-cost'].text.to_f*100).to_i
616
+ currency = element.elements['shipping-cost'].attributes['currency']
617
+ result.cost = Money.new(amount, currency)
618
+
619
+ return result
620
+ end
621
+ end
622
+
623
+ # OrderAdjustment objects contain the adjustments (i.e. the entities in the cart that
624
+ # represent positive and negative amounts (at the moment Google Checkout support coupons,
625
+ # gift certificates and shipping)).
626
+ class OrderAdjustment
627
+ # The <adjustment-total> tag contains the total adjustment to an order total based
628
+ # on tax, shipping, gift certificates and coupon codes (optional).
629
+ attr_accessor :adjustment_total
630
+
631
+ # Boolean, true iff the merchant calculations have been successful (optional).
632
+ attr_accessor :merchant_calculation_successful
633
+
634
+ # Array of MerchantCode objects.
635
+ attr_accessor :merchant_codes
636
+
637
+ # The chosen ShippingAdjustment object for this order.
638
+ attr_accessor :shipping
639
+
640
+ # The total amount of tax that has been paid for this order (Money, optional).
641
+ attr_accessor :total_tax
642
+
643
+ # Creates a new OrderAdjustment from a given REXML::Element object.
644
+ def self.create_from_element(element)
645
+ result = OrderAdjustment.new
646
+
647
+ amount = (element.elements['total-tax'].text.to_f*100).to_i rescue nil
648
+ currency = element.elements['total-tax'].attributes['currency'] rescue nil
649
+ result.total_tax = Money.new(amount, currency) unless amount.nil?
650
+
651
+ shipping_element = element.elements["shipping/*"]
652
+ result.shipping = ShippingAdjustment.create_from_element(shipping_element) unless shipping_element.nil?
653
+
654
+ result.merchant_codes = Array.new
655
+ element.elements.each(%q{merchant-codes/*}) { |elem| result.merchant_codes << MerchantCode.create_from_element(elem) }
656
+
657
+ result.merchant_calculation_successful = (element.elements['merchant-calculation-successful'].text.downcase == 'true') rescue nil
658
+
659
+ amount = (element.elements['adjustment-total'].text.to_f*100).to_i rescue nil
660
+ currency = element.elements['adjustment-total'].attributes['currency'] rescue nil
661
+ result.adjustment_total = Money.new(amount, currency) unless amount.nil?
662
+
663
+ return result
664
+ end
665
+ end
666
+
667
+ # Class with static methods to parse the <merchant-private(-item)-data> tags.
668
+ class PrivateDataParser
669
+ # Returns a Hash with the representation of the structure in
670
+ # item/merchant-private-item-data.
671
+ def self.element_to_value(element)
672
+ # The return value; We will iterate over all children below. When we see a REXML::Element
673
+ # child then we set result to a Hash if it is nil. After the result is a Hash, we will
674
+ # ignore all REXML::Text children.
675
+ # Otherwise, result will be set to the REXML::Text node's value if it is not whitespace
676
+ # only.
677
+ result = nil
678
+
679
+ element.each_child do |child|
680
+ case child
681
+ when REXML::Element
682
+ result ||= Hash.new
683
+ child_value = self.element_to_value(child)
684
+
685
+ # <foo>bar</foo> becomes 'foo' => 'bar
686
+ # <foo>foo</foo><foo>bar</foo> becomes 'foo' => [ 'foo', 'bar' ]
687
+ if result[child.name].nil? then
688
+ result[child.name] = child_value
689
+ elsif result[child.name].kind_of?(Array) then
690
+ result[child.name] << child_value
691
+ else
692
+ tmp = result[child.name]
693
+ result[child.name] = [ tmp, child_value ]
694
+ end
695
+ when REXML::Text
696
+ next if result.kind_of?(Hash) # ignore text if we already found a tag
697
+ str = child.value.strip
698
+ result = str if str.length > 0
699
+ else
700
+ # ignore
701
+ end
702
+ end
703
+
704
+ return result
705
+ end
706
+ end
707
+ end
708
+ end