Package gchecky :: Module controller
[hide private]
[frames] | no frames]

Source Code for Module gchecky.controller

  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   
6 -class ProcessingException(Exception):
7 - def __init__(self, message, where=''):
8 self.where = where 9 return Exception.__init__(self, message)
10
11 -class html_order(object):
12 """ 13 TODO: 14 """ 15 cart = None 16 signature = None 17 url = None 18 button = None 19 xml = None
20 - def html(self):
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
33 -class ControllerLevel_1(object):
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 # Specify all the needed information such as merchant account credentials: 58 # - sandbox or production 59 # - google vendor ID 60 # - google merchant key
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
67 - def _get_url(self, tag, diagnose):
68 urls = (self.is_sandbox and self.__SANDBOX_URLS 69 ) or self.__PRODUCTION_URLS 70 if urls.has_key(tag): 71 url = urls[tag] 72 if diagnose: 73 url += '/diagnose' 74 return url 75 raise Exception('Unknown url tag "' + tag + '"')
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
83 - def get_checkout_button_url(self, diagnose):
84 return self._get_url(self.__MERCHANT_BUTTON, diagnose) % (self.vendor_id,)
85 get_cart_post_button = get_checkout_button_url 86
87 - def get_order_processing_url(self, diagnose):
88 return self._get_url(self.__ORDER_PROCESSING, diagnose) % (self.vendor_id,)
89
90 - def get_client_donation_url(self, diagnose):
91 return self._get_url(self.__CLIENT_DONATION, diagnose) % (self.vendor_id,)
92
93 - def get_server_donation_url(self, diagnose):
94 return self._get_url(self.__SERVER_DONATION, diagnose) % (self.vendor_id,)
95
96 - def get_donation_button_url(self, diagnose):
97 return self._get_url(self.__DONATION_BUTTON, diagnose) % (self.vendor_id,)
98
99 - def create_HMAC_SHA_signature(self, xml_text):
100 import hmac, sha 101 return hmac.new(self.merchant_key, xml_text, sha).digest()
102 103 # Specify order_id to track the order 104 # The order_id will be send back to us by google with order verification
105 - def prepare_order(self, order, order_id=None, diagnose=False):
106 cart = order.toxml() 107 108 cart64 = b64encode(cart) 109 signature64 = b64encode(self.create_HMAC_SHA_signature(cart)) 110 html = html_order() 111 html.cart = cart64 112 html.signature = signature64 113 html.url = self.get_client_post_cart_url(diagnose) 114 html.button = self.get_checkout_button_url(diagnose) 115 html.xml = cart 116 return html
117
118 - def prepare_donation(self, order, order_id=None, diagnose=False):
123
124 -class ControllerContext(object):
125 """ 126 """ 127 # Indicates the direction: True => we call GC, False => GC calls us 128 outgoing = True 129 # The request XML text 130 xml = None 131 # The request message - one of the classes in gchecky.model module 132 message = None 133 # Indicates that the message being sent is diagnose message (implies outgoing=True). 134 diagnose = False 135 # Associated google order number 136 order_id = None 137 # A serial number assigned by google to this message 138 serial = None 139 # The response message - one of the classes in gchecky.model module 140 response_message = None 141 # The response XML text 142 response_xml = None 143
144 - def __init__(self, outgoing = True):
145 self.outgoing = outgoing
146
147 -class GcheckyError(Exception):
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
168 - def __unicode__(self):
169 return self.message
170 __str__ = __unicode__ 171 __repr__ = __unicode__
172
173 -class DataError(GcheckyError):
174 """ 175 An exception of this class occures whenever there is error in converting 176 python data to/from xml. 177 """ 178 pass
179
180 -class HandlerError(GcheckyError):
181 """ 182 An exception of this class occures whenever an exception is thrown 183 from user defined handler. 184 """ 185 pass
186
187 -class SystemError(GcheckyError):
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
194 -class LibraryError(GcheckyError):
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
202 -class ControllerLevel_2(ControllerLevel_1):
203 - def on_xml_sending(self, context):
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
213 - def on_xml_sent(self, context):
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
224 - def on_message_sending(self, context):
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
234 - def on_message_sent(self, context):
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
245 - def on_xml_receiving(self, context):
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
254 - def on_xml_received(self, context):
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
264 - def on_message_receiving(self, context):
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
273 - def on_message_received(self, context):
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
283 - def on_retrieve_order(self, order_id, context=None):
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
298 - def handle_new_order(self, message, order_id, context, order=None):
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
309 - def handle_order_state_change(self, message, order_id, context, order=None):
310 pass
311
312 - def handle_authorization_amount(self, message, order_id, context, order=None):
313 pass
314
315 - def handle_risk_information(self, message, order_id, context, order=None):
316 """ 317 Google Checkout sends a risk information notification to provide 318 financial information 319 that helps you to ensure that an order is not fraudulent. 320 """ 321 pass
322
323 - def handle_charge_amount(self, message, order_id, context, order=None):
324 pass
325
326 - def handle_refund_amount(self, message, order_id, context, order=None):
327 pass
328
329 - def handle_chargeback_amount(self, message, order_id, context, order=None):
330 pass
331
332 - def handle_notification(self, message, order_id, context, order=None):
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 # By default return None because we don' handle anything 348 pass
349
350 - def on_exception(self, exception, context):
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
359 - def __call_handler(self, handler_name, context, *args, **kwargs):
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
425 - def __process_message_result(self, response_xml, context):
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 # It has to be a 'diagnosis' response, otherwise... omg!.. panic!... 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 # If the response is 'ok' or 'bye' just return, because its good 441 if doc.__class__ == gmodel.request_received_t: 442 return doc 443 444 if doc.__class__ == gmodel.bye_t: 445 return doc 446 447 # It's not 'ok' so it has to be 'error', otherwise it's an error 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 # 'error' - process it by throwing an exception with error/warning text 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
461 - def hello(self):
462 context = ControllerContext() 463 doc = self.send_message(gmodel.hello_t(), context) 464 if isinstance(doc, gxml.Document) and (doc.__class__ != gmodel.bye_t): 465 error = "Expected <bye/> but got %s" % (doc.__class__,) 466 raise LibraryError(message=error, context=context, origin=e)
467
468 - def archive_order(self, order_id):
471
472 - def unarchive_order(self, order_id):
475
476 - def send_buyer_message(self, order_id, message):
477 self.send_message(gmodel.send_buyer_message_t( 478 google_order_number = order_id, 479 message = message, 480 send_email = True 481 ))
482
483 - def add_merchant_order_number(self, order_id, merchant_order_number):
488
489 - def add_tracking_data(self, order_id, carrier, tracking_number):
495
496 - def charge_order(self, order_id, amount):
497 self.send_message(gmodel.charge_order_t( 498 google_order_number = order_id, 499 amount = gmodel.price_t(value = amount, currency = self.currency) 500 ))
501
502 - def refund_order(self, order_id, amount, reason, comment=None):
503 self.send_message(gmodel.refund_order_t( 504 google_order_number = order_id, 505 amount = gmodel.price_t(value = amount, currency = self.currency), 506 reason = reason, 507 comment = comment or None 508 ))
509
510 - def authorize_order(self, order_id):
514
515 - def cancel_order(self, order_id, reason, comment=None):
516 self.send_message(gmodel.cancel_order_t( 517 google_order_number = order_id, 518 reason = reason, 519 comment = comment or None 520 ))
521
522 - def process_order(self, order_id):
526
527 - def deliver_order(self, order_id, 528 carrier = None, tracking_number = None, 529 send_email = None):
530 tracking = None 531 if carrier or tracking_number: 532 tracking = gmodel.tracking_data_t(carrier = carrier, 533 tracking_number = tracking_number) 534 self.send_message(gmodel.deliver_order_t( 535 google_order_number = order_id, 536 tracking_data = tracking, 537 send_email = send_email or None 538 ))
539 540 # This method gets a string and returns a string
541 - def receive_xml(self, input_xml, context=None):
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 # A dictionary of document handler names. Comes handy in receive_message. 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
581 - def receive_message(self, message, order_id, context):
582 context.order_id = order_id 583 self.__call_handler('on_message_receiving', context=context) 584 # retreive order instance from DB given the google order number 585 order = self.__call_handler('on_retrieve_order', context=context, order_id=order_id) 586 587 # handler = None 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 # TODO: Remove this after testing 620 assert result.__class__ is gmodel.ok_t 621 622 self.__call_handler('on_message_received', context=context) 623 return result
624 625 # Just an alias with a shorter name. 626 Controller = ControllerLevel_2 627