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: 91 $
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
91 """
92 >>> 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',
93 ... merchant_private_item_data=['some', {'private':'data', 'to':['test','the'],'thing':None}, '!! Numbers: ', None, False, True, [11, 12., [13.4]]])
94 ... )
95 >>> test_node(item_t(name='Peter', description='The Great', unit_price=price_t(value=1, currency='GBP'), quantity=1))
96 """
97 name = gxml.String('item-name')
98 description = gxml.String('item-description')
99 unit_price = gxml.Complex('unit-price', price_t)
100 quantity = gxml.Decimal('quantity')
101 merchant_item_id = gxml.String('merchant-item-id', required=False)
102 tax_table_selector = gxml.String('tax-table-selector', required=False)
103 digital_content = gxml.Complex('digital-content', digital_content_t, required=False)
104 merchant_private_item_data = gxml.Any('merchant-private-item-data',
105 save_node_and_xml=True,
106 required=False)
107
108 -class postal_area_t(gxml.Node):
109 """
110 >>> test_node(postal_area_t(country_code = 'VU'),
111 ... '''
112 ... <node><country-code>VU</country-code></node>
113 ... '''
114 ... )
115 """
116 country_code = CountryCode('country-code')
117 postal_code_pattern = gxml.String('postal-code-pattern', required=False)
118
125
127 """
128 Represents a list of regions.
129
130 >>> test_node(
131 ... areas_t(
132 ... states = ['LA', 'NY'],
133 ... country_areas = ['ALL', 'CONTINENTAL_48']
134 ... )
135 ... ,
136 ... '''
137 ... <node>
138 ... <us-state-area>
139 ... <state>LA</state>
140 ... </us-state-area>
141 ... <us-state-area>
142 ... <state>NY</state>
143 ... </us-state-area>
144 ... <us-country-area>
145 ... <country-area>ALL</country-area>
146 ... </us-country-area>
147 ... <us-country-area>
148 ... <country-area>CONTINENTAL_48</country-area>
149 ... </us-country-area>
150 ... </node>
151 ... '''
152 ... )
153 """
154 states = gxml.List('', gxml.String('us-state-area/state'), required=False)
155 zip_patterns = gxml.List('', gxml.String('us-zip-area/zip-pattern'), required=False)
156 country_areas = gxml.List('', gxml.String('us-country-area/country-area'), values=('CONTINENTAL_48', 'FULL_50_STATES', 'ALL'), required=False)
157
161
164
168
171
174
177
182
187
192
194 """
195 Represents information about shipping costs.
196
197 >>> test_node(
198 ... shipping_option_t(
199 ... name = 'Testing',
200 ... price = price_t(
201 ... currency = 'GBP',
202 ... value = 9.99,
203 ... ),
204 ... allowed_areas = allowed_areas_t(
205 ... world_area = True,
206 ... ),
207 ... excluded_areas = excluded_areas_t(
208 ... postal_areas = [postal_area_t(
209 ... country_code = 'US',
210 ... )],
211 ... ),
212 ... )
213 ... , '''
214 ... <node name='Testing'>
215 ... <price currency='GBP'>9.990</price>
216 ... <shipping-restrictions>
217 ... <allowed-areas>
218 ... <world-area/>
219 ... </allowed-areas>
220 ... <excluded-areas>
221 ... <postal-area>
222 ... <country-code>US</country-code>
223 ... </postal-area>
224 ... </excluded-areas>
225 ... </shipping-restrictions>
226 ... </node>
227 ... ''')
228 """
229 name = gxml.String('@name')
230 price = gxml.Complex('price', price_t)
231 allowed_areas = gxml.Complex('shipping-restrictions/allowed-areas', allowed_areas_t, required=False)
232 excluded_areas = gxml.Complex('shipping-restrictions/excluded-areas', excluded_areas_t, required=False)
233
238
242
247
248 URL_PARAMETER_TYPES=(
249 'buyer-id',
250 'order-id',
251 'order-subtotal',
252 'order-subtotal-plus-tax',
253 'order-subtotal-plus-shipping',
254 'order-total',
255 'tax-amount',
256 'shipping-amount',
257 'coupon-amount',
258 'billing-city',
259 'billing-region',
260 'billing-postal-code',
261 'billing-country-code',
262 'shipping-city',
263 'shipping-region',
264 'shipping-postal-code',
265 'shipping-country-code',
266 )
267
271
275
277 """
278 >>> test_node(
279 ... checkout_flow_support_t(
280 ... parameterized_urls = [
281 ... parameterized_url_t(
282 ... url='http://google.com/',
283 ... parameters=[url_parameter_t(name='a', type='buyer-id')]
284 ... ),
285 ... parameterized_url_t(
286 ... url='http://yahoo.com/',
287 ... parameters=[url_parameter_t(name='a', type='shipping-city'),
288 ... url_parameter_t(name='b', type='tax-amount')]
289 ... ),
290 ... parameterized_url_t(
291 ... url='http://mozilla.com/',
292 ... parameters=[url_parameter_t(name='a', type='order-total'),
293 ... url_parameter_t(name='b', type='shipping-region'),
294 ... url_parameter_t(name='c', type='shipping-country-code')]
295 ... )
296 ... ],
297 ... )
298 ... ,
299 ... '''
300 ... <node>
301 ... <parameterized-urls>
302 ... <parameterized-url url="http://google.com/">
303 ... <parameters>
304 ... <url-parameter name="a" type="buyer-id"/>
305 ... </parameters>
306 ... </parameterized-url>
307 ... <parameterized-url url="http://yahoo.com/">
308 ... <parameters>
309 ... <url-parameter name="a" type="shipping-city"/>
310 ... <url-parameter name="b" type="tax-amount"/>
311 ... </parameters>
312 ... </parameterized-url>
313 ... <parameterized-url url="http://mozilla.com/">
314 ... <parameters>
315 ... <url-parameter name="a" type="order-total"/>
316 ... <url-parameter name="b" type="shipping-region"/>
317 ... <url-parameter name="c" type="shipping-country-code"/>
318 ... </parameters>
319 ... </parameterized-url>
320 ... </parameterized-urls>
321 ... </node>
322 ... '''
323 ... )
324 """
325 edit_cart_url = gxml.Url('edit-cart-url', required=False)
326 continue_shopping_url = gxml.Url('continue-shopping-url', required=False)
327 tax_tables = gxml.Complex('tax-tables', tax_tables_t, required=False)
328 shipping_methods = gxml.Complex('shipping-methods', shipping_methods_t, required=False)
329 merchant_calculations = gxml.Complex('merchant-calculations', merchant_calculations_t, required=False)
330 request_buyer_phone_number = gxml.Boolean('request-buyer-phone-number', required=False)
331 platform_id = gxml.Long('platform-id', required=False)
332 analytics_data = gxml.String('analytics-data', required=False)
333 parameterized_urls = gxml.List('parameterized-urls', gxml.Complex('parameterized-url', parameterized_url_t), required=False)
334
341
343 """
344 Represents a simple test that verifies that your server communicates
345 properly with Google Checkout. The fourth step of
346 the U{Getting Started with Google Checkout<http://code.google.com/apis/checkout/developer/index.html#integration_overview>}
347 section of the Developer's Guide explains how to execute this test.
348
349 >>> test_document(hello_t(),
350 ... "<hello xmlns='http://checkout.google.com/schema/2'/>"
351 ... )
352 """
353 tag_name='hello'
354
355 -class bye_t(gxml.Document):
356 """
357 Represents a response that indicates that Google correctly received
358 a <hello> request.
359
360 >>> test_document(
361 ... bye_t(serial_number="7315dacf-3a2e-80d5-aa36-8345cb54c143")
362 ... ,
363 ... '''
364 ... <bye xmlns="http://checkout.google.com/schema/2"
365 ... serial-number="7315dacf-3a2e-80d5-aa36-8345cb54c143" />
366 ... '''
367 ... )
368 """
369 tag_name = 'bye'
370 serial_number = gxml.ID('@serial-number')
371
377
383
387
391
392
393
394
399
402
409
422
430
440
441 FINANCIAL_ORDER_STATE=('REVIEWING', 'CHARGEABLE', 'CHARGING', 'CHARGED', 'PAYMENT_DECLINED', 'CANCELLED', 'CANCELLED_BY_GOOGLE')
442 FULFILLMENT_ORDER_STATE=('NEW', 'PROCESSING', 'DELIVERED', 'WILL_NOT_DELIVER')
443
455
457 """
458 Try doctests:
459 >>> a = checkout_redirect_t(serial_number='blabla12345',
460 ... redirect_url='http://www.somewhere.com')
461 >>> b = gxml.Document.fromxml(a.toxml())
462 >>> a == b
463 True
464 """
465 tag_name = 'checkout-redirect'
466 serial_number = gxml.ID('@serial-number')
467 redirect_url = gxml.Url('redirect-url')
468
470 tag_name = 'notification-acknowledgment'
471
479
480 AVS_VALUES=('Y', 'P', 'A', 'N', 'U')
481 CVN_VALUES=('M', 'N', 'U', 'E')
482
491
495
499
503
509
511 """
512 Represents an order that should be canceled. A <cancel-order> command
513 sets the financial-order-state and the fulfillment-order-state to canceled.
514
515 >>> test_document(
516 ... cancel_order_t(google_order_number = "841171949013218",
517 ... comment = 'Buyer found a better deal.',
518 ... reason = 'Buyer cancelled the order.'
519 ... )
520 ... ,
521 ... '''
522 ... <cancel-order xmlns="http://checkout.google.com/schema/2" google-order-number="841171949013218">
523 ... <comment>Buyer found a better deal.</comment>
524 ... <reason>Buyer cancelled the order.</reason>
525 ... </cancel-order>
526 ... '''
527 ... )
528 """
529 tag_name = 'cancel-order'
530 comment = gxml.String('comment', max_length=140, required=False)
531 reason = gxml.String('reason', max_length=140)
532
535
538
542
543 CARRIER_VALUES=('DHL', 'FedEx', 'UPS', 'USPS', 'Other')
544
548
553
555 """
556 Represents a tag containing a request to add a shipper's tracking number
557 to an order.
558
559 >>> test_document(
560 ... add_tracking_data_t(
561 ... google_order_number = '841171949013218',
562 ... tracking_data = tracking_data_t(
563 ... carrier = 'UPS',
564 ... tracking_number = 'Z9842W69871281267'
565 ... )
566 ... )
567 ... ,
568 ... '''
569 ... <add-tracking-data xmlns="http://checkout.google.com/schema/2"
570 ... google-order-number="841171949013218">
571 ... <tracking-data>
572 ... <tracking-number>Z9842W69871281267</tracking-number>
573 ... <carrier>UPS</carrier>
574 ... </tracking-data>
575 ... </add-tracking-data>
576 ... '''
577 ... )
578 """
579 tag_name='add-tracking-data'
580 tracking_data = gxml.Complex('tracking-data', tracking_data_t)
581
586
588 """
589 Represents a request to archive a particular order. You would archive
590 an order to remove it from your Merchant Center Inbox, indicating that
591 the order has been delivered.
592
593 >>> test_document(archive_order_t(google_order_number = '841171949013218'),
594 ... '''<archive-order xmlns="http://checkout.google.com/schema/2"
595 ... google-order-number="841171949013218" />'''
596 ... )
597 """
598 tag_name='archive-order'
599
602
604 """
605 Represents information about a successful charge for an order.
606
607 >>> from datetime import datetime
608 >>> import iso8601
609 >>> test_document(
610 ... charge_amount_notification_t(
611 ... serial_number='95d44287-12b1-4722-bc56-cfaa73f4c0d1',
612 ... google_order_number = '841171949013218',
613 ... timestamp = iso8601.parse_date('2006-03-18T18:25:31.593Z'),
614 ... latest_charge_amount = price_t(currency='USD', value=2226.06),
615 ... total_charge_amount = price_t(currency='USD', value=2226.06)
616 ... )
617 ... ,
618 ... '''
619 ... <charge-amount-notification xmlns="http://checkout.google.com/schema/2" serial-number="95d44287-12b1-4722-bc56-cfaa73f4c0d1">
620 ... <latest-charge-amount currency="USD">2226.060</latest-charge-amount>
621 ... <google-order-number>841171949013218</google-order-number>
622 ... <total-charge-amount currency="USD">2226.060</total-charge-amount>
623 ... <timestamp>2006-03-18T18:25:31.593000+00:00</timestamp>
624 ... </charge-amount-notification>
625 ... '''
626 ... )
627 """
628 tag_name='charge-amount-notification'
629 latest_charge_amount = gxml.Complex('latest-charge-amount', price_t)
630 total_charge_amount = gxml.Complex('total-charge-amount', price_t)
631
636
641
648
655
658
668
676
682
686
696
700
704
705
706
707 -class ok_t(gxml.Document):
709
711 """
712 Represents a response containing information about an invalid API request.
713 The information is intended to help you debug the problem causing the error.
714
715 >>> test_document(
716 ... error_t(serial_number = '3c394432-8270-411b-9239-98c2c499f87f',
717 ... error_message='Bad username and/or password for API Access.',
718 ... warning_messages = ['IP address is suspicious.',
719 ... 'MAC address is shadowed.']
720 ... )
721 ... ,
722 ... '''
723 ... <error xmlns="http://checkout.google.com/schema/2" serial-number="3c394432-8270-411b-9239-98c2c499f87f">
724 ... <error-message>Bad username and/or password for API Access.</error-message>
725 ... <warning-messages>
726 ... <string>IP address is suspicious.</string>
727 ... <string>MAC address is shadowed.</string>
728 ... </warning-messages>
729 ... </error>
730 ... '''
731 ... )
732 """
733 tag_name = 'error'
734 serial_number = gxml.ID('@serial-number')
735 error_message = gxml.String('error-message')
736 warning_messages = gxml.List('warning-messages',
737 gxml.String('string'),
738 required=False)
739
741 """
742 Represents a diagnostic response to an API request. The diagnostic
743 response contains the parsed XML in your request as well as any warnings
744 generated by your request.
745 Please see the U{Validating XML Messages to Google Checkout
746 <http://code.google.com/apis/checkout/developer/index.html#validating_xml_messages>}
747 section for more information about diagnostic requests and responses.
748 """
749 tag_name = 'diagnosis'
750 input_xml = gxml.Any('input-xml')
751 warnings = gxml.List('warnings',
752 gxml.String('string'),
753 required=False)
754
756 """
757 >>> test_document(
758 ... demo_failure_t(message='Demo Failure Message')
759 ... ,
760 ... '''<demo-failure xmlns="http://checkout.google.com/schema/2"
761 ... message="Demo Failure Message" />'''
762 ... )
763 """
764 tag_name = 'demo-failure'
765 message = gxml.String('@message', max_length=25)
766
767 if __name__ == "__main__":
769 import doctest
770 doctest.testmod()
771 run_doctests()
772