google4r-checkout-jn 1.1.jniziol

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