1 """
2 gchecky.model module describes the mapping between Google Checkout API (GC API)
3 XML messages (GC API XML Schema) and data classes.
4
5 This module uses L{gchecky.gxml} to automate much of the work so that
6 the actual logic is not cluttered with machinery.
7
8 The module code is simple and self-documenting. Please read the source code for
9 simple description of the data-structures. Note that it tries to follow exactly
10 the official GC API XML Schema.
11
12 All the comments for the GC API are at U{Google Chackout API documentation
13 <http://code.google.com/apis/checkout/developer/>}. Please consult it for any
14 questions about GC API functioning.
15
16 @author: etarassov
17 @version: $Revision: 129 $
18 @contact: gchecky at gmail
19 """
20
21 from gchecky import gxml
22 from gchecky.data import CountryCode, PresentOrNot
23
25 """
26 Method used in doctests: ensures that a document is properly serialized.
27 """
28
29 def normalize_xml(xml_text):
30 """
31 Normalize the xml text to canonical form, so that two xml text chunks
32 could be compared directly as strings.
33 """
34
35 if len(xml_text) < 2 or xml_text[0:2] != "<?":
36 xml_text = "<?xml version='1.0' encoding='UTF-8'?>\n" + xml_text
37 from xml.dom.minidom import parseString
38 doc = parseString(xml_text)
39
40 text = doc.toprettyxml('')
41
42
43
44 return text.replace('\n', '').replace(' ', '')
45
46 expected_xml = ((xml_text is not None) and normalize_xml(xml_text)) or None
47 obtained_xml = normalize_xml(doc.toxml(pretty=' '))
48
49 if expected_xml is not None and expected_xml != obtained_xml:
50 print "Expected:\n\n%s\n\nGot:\n\n%s\n" % (xml_text, doc.toxml(' '))
51
52 doc2 = gxml.Document.fromxml(doc.toxml())
53 if not (doc == doc2):
54 print '''
55 Failed to correctly interpret the generated XML for this document:
56 Original:
57 %s
58 Parsed:
59 %s
60 ''' % (doc.toxml(pretty=True), doc2.toxml())
61
63 """
64 Method used in doctests. Ensure that a node is properly serialized.
65 """
66 class Dummy(gxml.Document):
67 tag_name='dummy'
68 data=gxml.Complex('node', node.__class__, required=True)
69 if xml_text is not None:
70 xml_text = xml_text.replace('<', ' <')
71
72 xml_text = "<dummy xmlns='http://checkout.google.com/schema/2'>%s</dummy>" % (xml_text,)
73 test_document(Dummy(data=node), xml_text)
74
75 CURRENCIES = ('USD', 'GBP')
76
80
81 DISPLAY_DISPOSITION = ('OPTIMISTIC', 'PESSIMISTIC')
82 -class digital_content_t(gxml.Node):
83 description = gxml.Html('description', max_length=1024, required=False)
84 email_delivery = gxml.Boolean('email-delivery', required=False)
85 key = gxml.String('key', required=False)
86 url = gxml.String('url', required=False)
87 display_disposition = gxml.String('display-disposition', required=False,
88 values=DISPLAY_DISPOSITION)
89
93
95 """
96 >>> test_node(item_t(name='Peter', description='The Great', unit_price=price_t(value=1, currency='GBP'), quantity=1, merchant_item_id='custom_merchant_item_id',
97 ... merchant_private_item_data=['some', {'private':'data', 'to':['test','the'],'thing':None}, '!! Numbers: ', None, False, True, [11, 12., [13.4]]])
98 ... )
99 >>> test_node(item_t(name='Name with empty description', description='', unit_price=price_t(value=1, currency='GBP'), quantity=1))
100 """
101 name = gxml.String('item-name')
102 description = gxml.String('item-description')
103 unit_price = gxml.Complex('unit-price', price_t)
104 quantity = gxml.Decimal('quantity')
105 item_weight = gxml.Complex('item-weight', item_weight_t, required=False)
106 merchant_item_id = gxml.String('merchant-item-id', required=False)
107 tax_table_selector = gxml.String('tax-table-selector', required=False)
108 digital_content = gxml.Complex('digital-content', digital_content_t, required=False)
109 merchant_private_item_data = gxml.Any('merchant-private-item-data',
110 save_node_and_xml=True,
111 required=False)
112
113 -class postal_area_t(gxml.Node):
114 """
115 >>> test_node(postal_area_t(country_code = 'VU'),
116 ... '''
117 ... <node><country-code>VU</country-code></node>
118 ... '''
119 ... )
120 """
121 country_code = CountryCode('country-code')
122 postal_code_pattern = gxml.String('postal-code-pattern', required=False)
123
130
132 """
133 Represents a list of regions.
134
135 >>> test_node(
136 ... areas_t(
137 ... states = ['LA', 'NY'],
138 ... country_areas = ['ALL', 'CONTINENTAL_48']
139 ... )
140 ... ,
141 ... '''
142 ... <node>
143 ... <us-state-area>
144 ... <state>LA</state>
145 ... </us-state-area>
146 ... <us-state-area>
147 ... <state>NY</state>
148 ... </us-state-area>
149 ... <us-country-area>
150 ... <country-area>ALL</country-area>
151 ... </us-country-area>
152 ... <us-country-area>
153 ... <country-area>CONTINENTAL_48</country-area>
154 ... </us-country-area>
155 ... </node>
156 ... '''
157 ... )
158 """
159 states = gxml.List('', gxml.String('us-state-area/state'), required=False)
160 zip_patterns = gxml.List('', gxml.String('us-zip-area/zip-pattern'), required=False)
161 country_areas = gxml.List('', gxml.String('us-country-area/country-area'), values=('CONTINENTAL_48', 'FULL_50_STATES', 'ALL'), required=False)
162
166
169
174
177
180
183
188
193
198
200 """
201 Represents information about shipping costs.
202
203 >>> test_node(
204 ... shipping_option_t(
205 ... name = 'Testing',
206 ... price = price_t(
207 ... currency = 'GBP',
208 ... value = 9.99,
209 ... ),
210 ... allowed_areas = allowed_areas_t(
211 ... world_area = True,
212 ... ),
213 ... excluded_areas = excluded_areas_t(
214 ... postal_areas = [postal_area_t(
215 ... country_code = 'US',
216 ... )],
217 ... ),
218 ... )
219 ... , '''
220 ... <node name='Testing'>
221 ... <price currency='GBP'>9.990</price>
222 ... <shipping-restrictions>
223 ... <allowed-areas>
224 ... <world-area/>
225 ... </allowed-areas>
226 ... <excluded-areas>
227 ... <postal-area>
228 ... <country-code>US</country-code>
229 ... </postal-area>
230 ... </excluded-areas>
231 ... </shipping-restrictions>
232 ... </node>
233 ... ''')
234 """
235 name = gxml.String('@name')
236 price = gxml.Complex('price', price_t)
237 allowed_areas = gxml.Complex('shipping-restrictions/allowed-areas', allowed_areas_t, required=False)
238 excluded_areas = gxml.Complex('shipping-restrictions/excluded-areas', excluded_areas_t, required=False)
239
244
250
251
252
253
255 price = gxml.Complex('price', price_t)
256 shipping_company = gxml.String('shipping-company', values=('FedEx', 'UPS', 'USPS'))
257 carrier_pickup = gxml.String('carrier-pickup', values=('REGULAR_PICKUP',
258 'SPECIAL_PICKUP',
259 'DROP_OFF'),
260 default='DROP_OFF',
261 required=False)
262 shipping_type = gxml.String('shipping-type')
263 additional_fixed_charge = gxml.Complex('additional-fixed-charge', price_t, required=False)
264 additional_variable_charge_percent = gxml.Double('additional-variable-charge-percent', required=False)
265
269
276
285
293
297
299 carrier_calculated_shippings = gxml.List('', gxml.Complex('carrier-calculated-shipping', carrier_calculated_shipping_t), required=False)
300 flat_rate_shippings = gxml.List('', gxml.Complex('flat-rate-shipping', flat_rate_shipping_t), required=False)
301 merchant_calculated_shippings = gxml.List('', gxml.Complex('merchant-calculated-shipping', merchant_calculated_shipping_t), required=False)
302 pickups = gxml.List('', gxml.Complex('pickup', pickup_t), required=False)
303
304 URL_PARAMETER_TYPES=(
305 'buyer-id',
306 'order-id',
307 'order-subtotal',
308 'order-subtotal-plus-tax',
309 'order-subtotal-plus-shipping',
310 'order-total',
311 'tax-amount',
312 'shipping-amount',
313 'coupon-amount',
314 'billing-city',
315 'billing-region',
316 'billing-postal-code',
317 'billing-country-code',
318 'shipping-city',
319 'shipping-region',
320 'shipping-postal-code',
321 'shipping-country-code',
322 )
323
327
331
332
334 mode = gxml.String('mode', choices=('UP',
335 'DOWN',
336 'CEILING',
337 'FLOOR',
338 'HALF_UP',
339 'HALF_DOWN',
340 'HALF_EVEN',
341 'UNNECESSARY'))
342 rule = gxml.String('rule', choices=('PER_LINE',
343 'TOTAL'))
344
346 """
347 >>> test_node(
348 ... checkout_flow_support_t(
349 ... parameterized_urls = [
350 ... parameterized_url_t(
351 ... url='http://google.com/',
352 ... parameters=[url_parameter_t(name='a', type='buyer-id')]
353 ... ),
354 ... parameterized_url_t(
355 ... url='http://yahoo.com/',
356 ... parameters=[url_parameter_t(name='a', type='shipping-city'),
357 ... url_parameter_t(name='b', type='tax-amount')]
358 ... ),
359 ... parameterized_url_t(
360 ... url='http://mozilla.com/',
361 ... parameters=[url_parameter_t(name='a', type='order-total'),
362 ... url_parameter_t(name='b', type='shipping-region'),
363 ... url_parameter_t(name='c', type='shipping-country-code')]
364 ... )
365 ... ],
366 ... )
367 ... ,
368 ... '''
369 ... <node>
370 ... <parameterized-urls>
371 ... <parameterized-url url="http://google.com/">
372 ... <parameters>
373 ... <url-parameter name="a" type="buyer-id"/>
374 ... </parameters>
375 ... </parameterized-url>
376 ... <parameterized-url url="http://yahoo.com/">
377 ... <parameters>
378 ... <url-parameter name="a" type="shipping-city"/>
379 ... <url-parameter name="b" type="tax-amount"/>
380 ... </parameters>
381 ... </parameterized-url>
382 ... <parameterized-url url="http://mozilla.com/">
383 ... <parameters>
384 ... <url-parameter name="a" type="order-total"/>
385 ... <url-parameter name="b" type="shipping-region"/>
386 ... <url-parameter name="c" type="shipping-country-code"/>
387 ... </parameters>
388 ... </parameterized-url>
389 ... </parameterized-urls>
390 ... </node>
391 ... '''
392 ... )
393 """
394 edit_cart_url = gxml.Url('edit-cart-url', required=False)
395 continue_shopping_url = gxml.Url('continue-shopping-url', required=False)
396 tax_tables = gxml.Complex('tax-tables', tax_tables_t, required=False)
397 shipping_methods = gxml.Complex('shipping-methods', shipping_methods_t, required=False)
398 parameterized_urls = gxml.List('parameterized-urls', gxml.Complex('parameterized-url', parameterized_url_t), required=False)
399 merchant_calculations = gxml.Complex('merchant-calculations', merchant_calculations_t, required=False)
400 request_buyer_phone_number = gxml.Boolean('request-buyer-phone-number', required=False)
401 analytics_data = gxml.String('analytics-data', required=False)
402 platform_id = gxml.Long('platform-id', required=False)
403 rounding_policy = gxml.Complex('rounding-policy', rounding_policy_t, required=False)
404
411
413 """
414 Represents a simple test that verifies that your server communicates
415 properly with Google Checkout. The fourth step of
416 the U{Getting Started with Google Checkout<http://code.google.com/apis/checkout/developer/index.html#integration_overview>}
417 section of the Developer's Guide explains how to execute this test.
418
419 >>> test_document(hello_t(),
420 ... "<hello xmlns='http://checkout.google.com/schema/2'/>"
421 ... )
422 """
423 tag_name='hello'
424
425 -class bye_t(gxml.Document):
426 """
427 Represents a response that indicates that Google correctly received
428 a <hello> request.
429
430 >>> test_document(
431 ... bye_t(serial_number="7315dacf-3a2e-80d5-aa36-8345cb54c143")
432 ... ,
433 ... '''
434 ... <bye xmlns="http://checkout.google.com/schema/2"
435 ... serial-number="7315dacf-3a2e-80d5-aa36-8345cb54c143" />
436 ... '''
437 ... )
438 """
439 tag_name = 'bye'
440 serial_number = gxml.ID('@serial-number')
441
447
453
457
461
465
466
467
468
483
486
493
497
499 address1 = gxml.String('address1')
500 address2 = gxml.String('address2', required=False)
501 city = gxml.String('city')
502 company_name = gxml.String('company-name', required=False)
503 contact_name = gxml.String('contact-name', required=False)
504 country_code = gxml.String('country-code')
505 email = gxml.Email('email', required=False)
506 fax = gxml.Phone('fax', required=False, empty=True)
507 phone = gxml.Phone('phone', required=False, empty=True)
508 postal_code = gxml.Zip('postal-code')
509 region = gxml.String('region', empty=True)
510 structured_name = gxml.Complex('structured-name',
511 structured_name_t, required=False)
512
521
531
537
538 FINANCIAL_ORDER_STATE=('REVIEWING', 'CHARGEABLE', 'CHARGING', 'CHARGED', 'PAYMENT_DECLINED', 'CANCELLED', 'CANCELLED_BY_GOOGLE')
539 FULFILLMENT_ORDER_STATE=('NEW', 'PROCESSING', 'DELIVERED', 'WILL_NOT_DELIVER')
540
542 tag_name = 'new-order-notification'
543 buyer_billing_address = gxml.Complex('buyer-billing-address', buyer_billing_address_t)
544 buyer_id = gxml.Long('buyer-id')
545 buyer_marketing_preferences = gxml.Complex('buyer-marketing-preferences', buyer_marketing_preferences_t)
546 buyer_shipping_address = gxml.Complex('buyer-shipping-address', buyer_shipping_address_t)
547 financial_order_state = gxml.String('financial-order-state', values=FINANCIAL_ORDER_STATE)
548 fulfillment_order_state = gxml.String('fulfillment-order-state', values=FULFILLMENT_ORDER_STATE)
549 order_adjustment = gxml.Complex('order-adjustment', order_adjustment_t)
550 order_total = gxml.Complex('order-total', price_t)
551 shopping_cart = gxml.Complex('shopping-cart', shopping_cart_t)
552 promotions = gxml.List('promotions',
553 gxml.Complex('promotion', promotion_t),
554 required=False)
555
557 """
558 Try doctests:
559 >>> a = checkout_redirect_t(serial_number='blabla12345',
560 ... redirect_url='http://www.somewhere.com')
561 >>> b = gxml.Document.fromxml(a.toxml())
562 >>> a == b
563 True
564 """
565 tag_name = 'checkout-redirect'
566 serial_number = gxml.ID('@serial-number')
567 redirect_url = gxml.Url('redirect-url')
568
572
580
581 AVS_VALUES=('Y', 'P', 'A', 'N', 'U')
582 CVN_VALUES=('M', 'N', 'U', 'E')
583
592
596
600
604
610
612 """
613 Represents an order that should be canceled. A <cancel-order> command
614 sets the financial-order-state and the fulfillment-order-state to canceled.
615
616 >>> test_document(
617 ... cancel_order_t(google_order_number = "841171949013218",
618 ... comment = 'Buyer found a better deal.',
619 ... reason = 'Buyer cancelled the order.'
620 ... )
621 ... ,
622 ... '''
623 ... <cancel-order xmlns="http://checkout.google.com/schema/2" google-order-number="841171949013218">
624 ... <comment>Buyer found a better deal.</comment>
625 ... <reason>Buyer cancelled the order.</reason>
626 ... </cancel-order>
627 ... '''
628 ... )
629 """
630 tag_name = 'cancel-order'
631 comment = gxml.String('comment', max_length=140, required=False)
632 reason = gxml.String('reason', max_length=140)
633
636
639
643
644 CARRIER_VALUES=('DHL', 'FedEx', 'UPS', 'USPS', 'Other')
645
649
654
656 """
657 Represents a tag containing a request to add a shipper's tracking number
658 to an order.
659
660 >>> test_document(
661 ... add_tracking_data_t(
662 ... google_order_number = '841171949013218',
663 ... tracking_data = tracking_data_t(
664 ... carrier = 'UPS',
665 ... tracking_number = 'Z9842W69871281267'
666 ... )
667 ... )
668 ... ,
669 ... '''
670 ... <add-tracking-data xmlns="http://checkout.google.com/schema/2"
671 ... google-order-number="841171949013218">
672 ... <tracking-data>
673 ... <tracking-number>Z9842W69871281267</tracking-number>
674 ... <carrier>UPS</carrier>
675 ... </tracking-data>
676 ... </add-tracking-data>
677 ... '''
678 ... )
679 """
680 tag_name='add-tracking-data'
681 tracking_data = gxml.Complex('tracking-data', tracking_data_t)
682
687
689 """
690 Represents a request to archive a particular order. You would archive
691 an order to remove it from your Merchant Center Inbox, indicating that
692 the order has been delivered.
693
694 >>> test_document(archive_order_t(google_order_number = '841171949013218'),
695 ... '''<archive-order xmlns="http://checkout.google.com/schema/2"
696 ... google-order-number="841171949013218" />'''
697 ... )
698 """
699 tag_name='archive-order'
700
703
705 """
706 Represents information about a successful charge for an order.
707
708 >>> from datetime import datetime
709 >>> import iso8601
710 >>> test_document(
711 ... charge_amount_notification_t(
712 ... serial_number='95d44287-12b1-4722-bc56-cfaa73f4c0d1',
713 ... google_order_number = '841171949013218',
714 ... timestamp = iso8601.parse_date('2006-03-18T18:25:31.593Z'),
715 ... latest_charge_amount = price_t(currency='USD', value=2226.06),
716 ... total_charge_amount = price_t(currency='USD', value=2226.06)
717 ... )
718 ... ,
719 ... '''
720 ... <charge-amount-notification xmlns="http://checkout.google.com/schema/2" serial-number="95d44287-12b1-4722-bc56-cfaa73f4c0d1">
721 ... <latest-charge-amount currency="USD">2226.060</latest-charge-amount>
722 ... <google-order-number>841171949013218</google-order-number>
723 ... <total-charge-amount currency="USD">2226.060</total-charge-amount>
724 ... <timestamp>2006-03-18T18:25:31.593000+00:00</timestamp>
725 ... </charge-amount-notification>
726 ... '''
727 ... )
728 """
729 tag_name='charge-amount-notification'
730 latest_charge_amount = gxml.Complex('latest-charge-amount',
731 price_t)
732 latest_promotion_charge_amount = gxml.Complex('latest-promotion-charge-amount',
733 price_t, required=False)
734 total_charge_amount = gxml.Complex('total-charge-amount', price_t)
735
742
749
756
763
766
776
784
790
794
804
808
812
813
814
815 -class ok_t(gxml.Document):
817
819 """
820 Represents a response containing information about an invalid API request.
821 The information is intended to help you debug the problem causing the error.
822
823 >>> test_document(
824 ... error_t(serial_number = '3c394432-8270-411b-9239-98c2c499f87f',
825 ... error_message='Bad username and/or password for API Access.',
826 ... warning_messages = ['IP address is suspicious.',
827 ... 'MAC address is shadowed.']
828 ... )
829 ... ,
830 ... '''
831 ... <error xmlns="http://checkout.google.com/schema/2" serial-number="3c394432-8270-411b-9239-98c2c499f87f">
832 ... <error-message>Bad username and/or password for API Access.</error-message>
833 ... <warning-messages>
834 ... <string>IP address is suspicious.</string>
835 ... <string>MAC address is shadowed.</string>
836 ... </warning-messages>
837 ... </error>
838 ... '''
839 ... )
840 """
841 tag_name = 'error'
842 serial_number = gxml.ID('@serial-number')
843 error_message = gxml.String('error-message')
844 warning_messages = gxml.List('warning-messages',
845 gxml.String('string'),
846 required=False)
847
849 """
850 Represents a diagnostic response to an API request. The diagnostic
851 response contains the parsed XML in your request as well as any warnings
852 generated by your request.
853 Please see the U{Validating XML Messages to Google Checkout
854 <http://code.google.com/apis/checkout/developer/index.html#validating_xml_messages>}
855 section for more information about diagnostic requests and responses.
856 """
857 tag_name = 'diagnosis'
858 input_xml = gxml.Any('input-xml')
859 warnings = gxml.List('warnings',
860 gxml.String('string'),
861 required=False)
862 serial_number = gxml.ID('@serial-number')
863
865 """
866 >>> test_document(
867 ... demo_failure_t(message='Demo Failure Message')
868 ... ,
869 ... '''<demo-failure xmlns="http://checkout.google.com/schema/2"
870 ... message="Demo Failure Message" />'''
871 ... )
872 """
873 tag_name = 'demo-failure'
874 message = gxml.String('@message', max_length=25)
875
878
883
890
895
900
905
912
913 if __name__ == "__main__":
915 import doctest
916 doctest.testmod()
917 run_doctests()
918