1 from base64 import b64encode
2 from django.utils.html import escape as html_escape
3 from gchecky import gxml
4 from gchecky import model as gmodel
5
10
12 """
13 TODO:
14 """
15 cart = None
16 signature = None
17 url = None
18 button = None
19 xml = None
21 """
22 Return the html form containing two required hidden fields
23 and the submit button in the form of Google Checkout button image.
24 """
25 return """
26 <form method="post" action="%s">
27 <input type="hidden" name="cart" value="%s" />
28 <input type="hidden" name="signature" value="%s" />
29 <input type="image" src="%s" alt="Google Checkout" />
30 </form>
31 """ % (html_escape(self.url), self.cart, self.signature, html_escape(self.button))
32
34 __MERCHANT_BUTTON = 'MERCHANT_BUTTON'
35 __CLIENT_POST_CART = 'CLIENT_POST_CART'
36 __SERVER_POST_CART = 'SERVER_POST_CART'
37 __ORDER_PROCESSING = 'ORDER_PROCESSING'
38 __CLIENT_DONATION = 'CLIENT_DONATION'
39 __SERVER_DONATION = 'SERVER_DONATION'
40 __DONATION_BUTTON = 'DONATION_BUTTON'
41 __SANDBOX_URLS = {__MERCHANT_BUTTON: 'https://sandbox.google.com/checkout/buttons/checkout.gif?merchant_id=%s&w=160&h=43&style=white&variant=text',
42 __CLIENT_POST_CART:'https://sandbox.google.com/checkout/api/checkout/v2/checkout/Merchant/%s',
43 __SERVER_POST_CART:'https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Merchant/%s',
44 __ORDER_PROCESSING:'https://sandbox.google.com/checkout/api/checkout/v2/request/Merchant/%s',
45 __CLIENT_DONATION: 'https://sandbox.google.com/checkout/api/checkout/v2/checkout/Donations/%s',
46 __SERVER_DONATION: 'https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Donations/%s',
47 __DONATION_BUTTON: 'https://sandbox.google.com/checkout/buttons/donation.gif?merchant_id=%s&w=160&h=43&style=white&variant=text',
48 }
49 __PRODUCTION_URLS={__MERCHANT_BUTTON: 'https://checkout.google.com/buttons/checkout.gif?merchant_id=%s&w=160&h=43&style=white&variant=text',
50 __CLIENT_POST_CART:'https://checkout.google.com/api/checkout/v2/checkout/Merchant/%s',
51 __SERVER_POST_CART:'https://checkout.google.com/api/checkout/v2/merchantCheckout/Merchant/%s',
52 __ORDER_PROCESSING:'https://checkout.google.com/api/checkout/v2/request/Merchant/%s',
53 __CLIENT_DONATION: 'https://checkout.google.com/api/checkout/v2/checkout/Donations/%s',
54 __SERVER_DONATION: 'https://checkout.google.com/api/checkout/v2/merchantCheckout/Donations/%s',
55 __DONATION_BUTTON: 'https://checkout.google.com/buttons/donation.gif?merchant_id=%s&w=160&h=43&style=white&variant=text',
56 }
57
58
59
60
61 - def __init__(self, vendor_id, merchant_key, is_sandbox=True, currency='USD'):
62 self.vendor_id = vendor_id
63 self.merchant_key = merchant_key
64 self.is_sandbox = is_sandbox
65 self.currency = currency
66
76
77 - def get_client_post_cart_url(self, diagnose):
78 return self._get_url(self.__CLIENT_POST_CART, diagnose) % (self.vendor_id,)
79
80 - def get_server_post_cart_url(self, diagnose):
81 return self._get_url(self.__SERVER_POST_CART, diagnose) % (self.vendor_id,)
82
85 get_cart_post_button = get_checkout_button_url
86
89
92
95
98
100 import hmac, sha
101 return hmac.new(self.merchant_key, xml_text, sha).digest()
102
103
104
117
123
124 -class ControllerContext(object):
125 """
126 """
127
128 outgoing = True
129
130 xml = None
131
132 message = None
133
134 diagnose = False
135
136 order_id = None
137
138 serial = None
139
140 response_message = None
141
142 response_xml = None
143
144 - def __init__(self, outgoing = True):
146
148 """
149 Base class for exception that could be thrown by gchecky library.
150 """
151 - def __init__(self, message, context, origin=None):
152 """
153 @param message String message describing the problem. Can't be empty.
154 @param context An instance of gchecky.controller.ControllerContext
155 that describes the current request processing context.
156 Can't be None.
157 @param origin The original exception that caused this exception
158 to be thrown if any. Could be None.
159 """
160 self.message = message
161 self.context = context
162 self.origin = origin
163 self.traceback = None
164 if origin is not None:
165 from traceback import format_exc
166 self.traceback = format_exc()
167
170 __str__ = __unicode__
171 __repr__ = __unicode__
172
174 """
175 An exception of this class occures whenever there is error in converting
176 python data to/from xml.
177 """
178 pass
179
181 """
182 An exception of this class occures whenever an exception is thrown
183 from user defined handler.
184 """
185 pass
186
188 """
189 An exception of this class occures whenever there is a system error, such
190 as network being unavailable or DB down.
191 """
192 pass
193
195 """
196 An exception of this class occures whenever there is a bug encountered
197 in gchecky library. It represents a bug which should be reported as an issue
198 at U{Gchecky issue tracker <http://gchecky.googlecode.com/>}.
199 """
200 pass
201
204 """
205 This hook is called just before sending xml to GC.
206
207 @param context.xml The xml message to be sent to GC.
208 @param context.url The exact URL the message is about to be sent.
209 @return Should return nothing, because the return value is ignored.
210 """
211 pass
212
214 """
215 This hook is called right after sending xml to GC.
216
217 @param context.xml The xml message to be sent to GC.
218 @param context.url The exact URL the message is about to be sent.
219 @param context.response_xml The reply xml of GC.
220 @return Should return nothing, because the return value is ignored.
221 """
222 pass
223
225 """
226 This hook is called just before sending xml to GC.
227
228 @param context.xml The xml message to be sent to GC.
229 @param context.url The exact URL the message is about to be sent.
230 @return Should return nothing, because the return value is ignored.
231 """
232 pass
233
235 """
236 This hook is called right after sending xml to GC.
237
238 @param context.xml The message to be sent to GC (an instance of one
239 of gchecky.model classes).
240 @param context.response_xml The reply message of GC.
241 @return Should return nothing, because the return value is ignored.
242 """
243 pass
244
246 """
247 This hook is called just before processing the received xml from GC.
248
249 @param context.xml The xml message received from GC.
250 @return Should return nothing, because the return value is ignored.
251 """
252 pass
253
255 """
256 This hook is called right after processing xml from GC.
257
258 @param context.xml The xml message received from GC.
259 @param context.response_xml The reply xml to GC.
260 @return Should return nothing, because the return value is ignored.
261 """
262 pass
263
265 """
266 This hook is called just before processing the received message from GC.
267
268 @param context.message The message received from GC.
269 @return Should return nothing, because the return value is ignored.
270 """
271 pass
272
274 """
275 This hook is called right after processing message from GC.
276
277 @param context.message The message received from GC.
278 @param context.response_message The reply object to GC (either ok_t or error_t).
279 @return Should return nothing, because the return value is ignored.
280 """
281 pass
282
284 """
285 This hook is called from message processing code just before calling
286 the corresponding message handler.
287 The idea is to allow user code to load order in one place and then
288 receive the loaded object as parameter in message handler.
289 This method should not throw if order is not found - instead it should
290 return None.
291
292 @param order_id The google order number corresponding to the message
293 received.
294 @return The order object that will be passed to message handlers.
295 """
296 pass
297
299 """
300 Google sends a new order notification when a buyer places an order
301 through Google Checkout. Before shipping the items in an order,
302 you should wait until you have also received the risk information
303 notification for that order as well as the order state change
304 notification informing you that the order's financial state
305 has been updated to 'CHARGEABLE'.
306 """
307 pass
308
311
314
322
325
328
331
333 """
334 This handler is called when a message received from GC and when the more
335 specific message handler was not found or returned None (which means
336 it was not able to process the message).
337
338 @param message The message from GC to be processed.
339 @param order_id The google order number for which message is sent.
340 @param order The object loaded by on_retrieve_order(order_id) or None.
341 @return If message was processed successfully then return gmodel.ok_t().
342 If an error occured when proessing, then the method should
343 return any other value (not-None).
344 If the message is of unknown type or can't be processed by
345 this handler then return None.
346 """
347
348 pass
349
351 """
352 By default simply rethrow the exception ignoring context.
353 Could be used for loggin all the processing errors.
354 @param exception The exception that was caught, of (sub)type GcheckyError.
355 @param context The request context where the exception occured.
356 """
357 raise exception
358
360 if hasattr(self, handler_name):
361 try:
362 handler = getattr(self, handler_name)
363 return handler(context=context, *args, **kwargs)
364 except Exception, e:
365 error = "Exception in user handler '%s': %s" % (handler_name, e)
366 raise HandlerError(message=error,
367 context=context,
368 origin=e)
369 error="Unknown user handler: '%s'" % (handler_name,)
370 raise HandlerError(message=error, context=context)
371
372 - def _send_xml(self, msg, context, diagnose):
373 """
374 The helper method that submits an xml message to GC.
375 """
376 context.diagnose = diagnose
377 url = self.get_order_processing_url(diagnose)
378 context.url = url
379 import urllib2
380 req = urllib2.Request(url=url, data=msg)
381 req.add_header('Authorization',
382 'Basic %s' % (b64encode('%s:%s' % (self.vendor_id,
383 self.merchant_key)),))
384 req.add_header('Content-Type', ' application/xml; charset=UTF-8')
385 req.add_header('Accept', ' application/xml; charset=UTF-8')
386 try:
387 self.__call_handler('on_xml_sending', context=context)
388 response = urllib2.urlopen(req).read()
389 self.__call_handler('on_xml_sent', context=context)
390 return response
391 except urllib2.HTTPError, e:
392 error = e.fp.read()
393 raise SystemError(message='Error in urllib2.urlopen: %s' % (error,),
394 context=context,
395 origin=e)
396
397 - def send_message(self, message, context=None, diagnose=False):
398 if context is None:
399 context = ControllerContext(outgoing=True)
400 context.message = message
401 context.diagnose = diagnose
402
403 if isinstance(message, gmodel.abstract_order_t):
404 context.order_id = message.google_order_number
405
406 try:
407 try:
408 self.__call_handler('on_message_sending', context=context)
409 message_xml = message.toxml()
410 context.xml = message_xml
411 except Exception, e:
412 error = "Error converting message to xml: '%s'" % (unicode(e), )
413 raise DataError(message=error, context=context, origin=e)
414 response_xml = self._send_xml(message_xml, context=context, diagnose=diagnose)
415 context.response_xml = response_xml
416
417 response = self.__process_message_result(response_xml, context=context)
418 context.response_message = response
419
420 self.__call_handler('on_message_sent', context=context)
421 return response
422 except GcheckyError, e:
423 return self.on_exception(exception=e, context=context)
424
426 try:
427 doc = gxml.Document.fromxml(response_xml)
428 except Exception, e:
429 error = "Error converting message to xml: '%s'" % (unicode(e), )
430 raise LibraryError(message=error, context=context, origin=e)
431
432 if context.diagnose:
433
434 if doc.__class__ != gmodel.diagnosis_t:
435 error = "The response has to be of type diagnosis_t, not '%s'" % (doc.__class__,)
436 raise LibraryError(message=error,
437 context=context)
438 return doc
439
440
441 if doc.__class__ == gmodel.request_received_t:
442 return doc
443
444 if doc.__class__ == gmodel.bye_t:
445 return doc
446
447
448 if doc.__class__ != gmodel.error_t:
449 error = "Unknown response type (expected error_t): '%s'" % (doc.__class__,)
450 raise LibraryError(message=error, context=context)
451
452
453 msg = 'Error message from GCheckout API:\n%s' % (doc.error_message, )
454 if doc.warning_messages:
455 tmp = ''
456 for warning in doc.warning_messages:
457 tmp += '\n%s' % (warning,)
458 msg += ('Additional warnings:%s' % (tmp,))
459 raise DataError(message=msg, context=context)
460
467
471
475
482
488
495
501
502 - def refund_order(self, order_id, amount, reason, comment=None):
509
514
521
526
527 - def deliver_order(self, order_id,
528 carrier = None, tracking_number = None,
529 send_email = None):
539
540
542 if context is None:
543 context = ControllerContext(outgoing=False)
544 context.xml = input_xml
545 try:
546 self.__call_handler('on_xml_receiving', context=context)
547 try:
548 input = gxml.Document.fromxml(input_xml)
549 context.message = input
550 except Exception, e:
551 error = 'Error reading XML: %s' % (e,)
552 raise DataError(message=error, context=context, origin=e)
553
554 result = self.receive_message(message=input,
555 order_id=input.google_order_number,
556 context=context)
557 context.response_message = result
558
559 try:
560 response_xml = result.toxml()
561 context.response_xml = response_xml
562 except Exception, e:
563 error = 'Error reading XML: %s' % (e,)
564 raise DataError(message=error, context=context, origin=e)
565 self.__call_handler('on_xml_received', context=context)
566 return response_xml
567 except GcheckyError, e:
568 return self.on_exception(exception=e, context=context)
569
570
571 __MESSAGE_HANDLERS = {
572 gmodel.new_order_notification_t: 'handle_new_order',
573 gmodel.order_state_change_notification_t: 'handle_order_state_change',
574 gmodel.authorization_amount_notification_t: 'handle_authorization_amount',
575 gmodel.risk_information_notification_t: 'handle_risk_information',
576 gmodel.charge_amount_notification_t: 'handle_charge_amount',
577 gmodel.refund_amount_notification_t: 'handle_refund_amount',
578 gmodel.chargeback_amount_notification_t: 'handle_chargeback_amount',
579 }
580
582 context.order_id = order_id
583 self.__call_handler('on_message_receiving', context=context)
584
585 order = self.__call_handler('on_retrieve_order', context=context, order_id=order_id)
586
587
588 result = None
589 if self.__MESSAGE_HANDLERS.has_key(message.__class__):
590 handler_name = self.__MESSAGE_HANDLERS[message.__class__]
591 result = self.__call_handler(handler_name,
592 message=message,
593 order_id=order_id,
594 context=context,
595 order=order)
596
597 if result is None:
598 result = self.__call_handler('handle_notification',
599 message=message,
600 order_id=order_id,
601 context=context,
602 order=order)
603
604 error = None
605 if result is None:
606 error = "Notification '%s' was not handled" % (message.__class__,)
607 elif not (result.__class__ is gmodel.ok_t):
608 try:
609 error = unicode(result)
610 except Exception, e:
611 error = "Invalid value returned by handler '%s': %s" % (handler_name,
612 e)
613 raise HandlerError(message=error, context=context, origin=e)
614
615 if error is not None:
616 result = gmodel.error_t(serial_number = 'error',
617 error_message=error)
618 else:
619
620 assert result.__class__ is gmodel.ok_t
621
622 self.__call_handler('on_message_received', context=context)
623 return result
624
625
626 Controller = ControllerLevel_2
627