from base64 import b64encode from gchecky import gxml from gchecky import model as gmodel class ProcessingException(Exception): def __init__(self, message, where=''): self.where = where return Exception.__init__(self, message) def html_escape(html): """ A simple helper that escapes '<', '>' and '&' to make sure the text is safe to be output directly into html text flow. @param html A (unicode or not) string to be escaped from html. >>> safe_str = 'Is''n it a text \"with\": some fancy characters ;-)?!' >>> html_escape(safe_str) == safe_str True >>> html_escape('Omg&, is not &a safe string &htmlize >_', '>') class html_order(object): """ TODO: """ cart = None signature = None url = None button = None xml = None def html(self): """ Return the html form containing two required hidden fields and the submit button in the form of Google Checkout button image. """ return """
""" % (html_escape(self.url), self.cart, self.signature, html_escape(self.button)) class ControllerLevel_1(object): __MERCHANT_BUTTON = 'MERCHANT_BUTTON' __CLIENT_POST_CART = 'CLIENT_POST_CART' __SERVER_POST_CART = 'SERVER_POST_CART' __ORDER_PROCESSING = 'ORDER_PROCESSING' __CLIENT_DONATION = 'CLIENT_DONATION' __SERVER_DONATION = 'SERVER_DONATION' __DONATION_BUTTON = 'DONATION_BUTTON' __SANDBOX_URLS = {__MERCHANT_BUTTON: 'https://sandbox.google.com/checkout/buttons/checkout.gif?merchant_id=%s&w=160&h=43&style=white&variant=text', __CLIENT_POST_CART:'https://sandbox.google.com/checkout/api/checkout/v2/checkout/Merchant/%s', __SERVER_POST_CART:'https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Merchant/%s', __ORDER_PROCESSING:'https://sandbox.google.com/checkout/api/checkout/v2/request/Merchant/%s', __CLIENT_DONATION: 'https://sandbox.google.com/checkout/api/checkout/v2/checkout/Donations/%s', __SERVER_DONATION: 'https://sandbox.google.com/checkout/api/checkout/v2/merchantCheckout/Donations/%s', __DONATION_BUTTON: 'https://sandbox.google.com/checkout/buttons/donation.gif?merchant_id=%s&w=160&h=43&style=white&variant=text', } __PRODUCTION_URLS={__MERCHANT_BUTTON: 'https://checkout.google.com/buttons/checkout.gif?merchant_id=%s&w=160&h=43&style=white&variant=text', __CLIENT_POST_CART:'https://checkout.google.com/api/checkout/v2/checkout/Merchant/%s', __SERVER_POST_CART:'https://checkout.google.com/api/checkout/v2/merchantCheckout/Merchant/%s', __ORDER_PROCESSING:'https://checkout.google.com/api/checkout/v2/request/Merchant/%s', __CLIENT_DONATION: 'https://checkout.google.com/api/checkout/v2/checkout/Donations/%s', __SERVER_DONATION: 'https://checkout.google.com/api/checkout/v2/merchantCheckout/Donations/%s', __DONATION_BUTTON: 'https://checkout.google.com/buttons/donation.gif?merchant_id=%s&w=160&h=43&style=white&variant=text', } # Specify all the needed information such as merchant account credentials: # - sandbox or production # - google vendor ID # - google merchant key def __init__(self, vendor_id, merchant_key, is_sandbox=True, currency='USD'): self.vendor_id = vendor_id self.merchant_key = merchant_key self.is_sandbox = is_sandbox self.currency = currency def _get_url(self, tag, diagnose): urls = (self.is_sandbox and self.__SANDBOX_URLS ) or self.__PRODUCTION_URLS if urls.has_key(tag): url = urls[tag] if diagnose: url += '/diagnose' return url raise Exception('Unknown url tag "' + tag + '"') def get_client_post_cart_url(self, diagnose): return self._get_url(self.__CLIENT_POST_CART, diagnose) % (self.vendor_id,) def get_server_post_cart_url(self, diagnose): return self._get_url(self.__SERVER_POST_CART, diagnose) % (self.vendor_id,) def get_checkout_button_url(self, diagnose): return self._get_url(self.__MERCHANT_BUTTON, diagnose) % (self.vendor_id,) get_cart_post_button = get_checkout_button_url def get_order_processing_url(self, diagnose): return self._get_url(self.__ORDER_PROCESSING, diagnose) % (self.vendor_id,) def get_client_donation_url(self, diagnose): return self._get_url(self.__CLIENT_DONATION, diagnose) % (self.vendor_id,) def get_server_donation_url(self, diagnose): return self._get_url(self.__SERVER_DONATION, diagnose) % (self.vendor_id,) def get_donation_button_url(self, diagnose): return self._get_url(self.__DONATION_BUTTON, diagnose) % (self.vendor_id,) def create_HMAC_SHA_signature(self, xml_text): import hmac, sha return hmac.new(self.merchant_key, xml_text, sha).digest() # Specify order_id to track the order # The order_id will be send back to us by google with order verification def prepare_order(self, order, order_id=None, diagnose=False): cart = order.toxml() cart64 = b64encode(cart) signature64 = b64encode(self.create_HMAC_SHA_signature(cart)) html = html_order() html.cart = cart64 html.signature = signature64 html.url = self.get_client_post_cart_url(diagnose) html.button = self.get_checkout_button_url(diagnose) html.xml = cart return html def prepare_donation(self, order, order_id=None, diagnose=False): html = self.prepare_order(order, order_id, diagnose) html.url = self.get_client_donation_url(diagnose) html.button = self.get_donation_button_url(diagnose) return html class ControllerContext(object): """ """ # Indicates the direction: True => we call GC, False => GC calls us outgoing = True # The request XML text xml = None # The request message - one of the classes in gchecky.model module message = None # Indicates that the message being sent is diagnose message (implies outgoing=True). diagnose = False # Associated google order number order_id = None # A serial number assigned by google to this message serial = None # The response message - one of the classes in gchecky.model module response_message = None # The response XML text response_xml = None def __init__(self, outgoing = True): self.outgoing = outgoing class GcheckyError(Exception): """ Base class for exception that could be thrown by gchecky library. """ def __init__(self, message, context, origin=None): """ @param message String message describing the problem. Can't be empty. @param context An instance of gchecky.controller.ControllerContext that describes the current request processing context. Can't be None. @param origin The original exception that caused this exception to be thrown if any. Could be None. """ self.message = message self.context = context self.origin = origin self.traceback = None if origin is not None: from traceback import format_exc self.traceback = format_exc() def __unicode__(self): return self.message __str__ = __unicode__ __repr__ = __unicode__ class DataError(GcheckyError): """ An exception of this class occures whenever there is error in converting python data to/from xml. """ pass class HandlerError(GcheckyError): """ An exception of this class occures whenever an exception is thrown from user defined handler. """ pass class SystemError(GcheckyError): """ An exception of this class occures whenever there is a system error, such as network being unavailable or DB down. """ pass class LibraryError(GcheckyError): """ An exception of this class occures whenever there is a bug encountered in gchecky library. It represents a bug which should be reported as an issue at U{Gchecky issue tracker }. """ pass class ControllerLevel_2(ControllerLevel_1): def on_xml_sending(self, context): """ This hook is called just before sending xml to GC. @param context.xml The xml message to be sent to GC. @param context.url The exact URL the message is about to be sent. @return Should return nothing, because the return value is ignored. """ pass def on_xml_sent(self, context): """ This hook is called right after sending xml to GC. @param context.xml The xml message to be sent to GC. @param context.url The exact URL the message is about to be sent. @param context.response_xml The reply xml of GC. @return Should return nothing, because the return value is ignored. """ pass def on_message_sending(self, context): """ This hook is called just before sending xml to GC. @param context.xml The xml message to be sent to GC. @param context.url The exact URL the message is about to be sent. @return Should return nothing, because the return value is ignored. """ pass def on_message_sent(self, context): """ This hook is called right after sending xml to GC. @param context.xml The message to be sent to GC (an instance of one of gchecky.model classes). @param context.response_xml The reply message of GC. @return Should return nothing, because the return value is ignored. """ pass def on_xml_receiving(self, context): """ This hook is called just before processing the received xml from GC. @param context.xml The xml message received from GC. @return Should return nothing, because the return value is ignored. """ pass def on_xml_received(self, context): """ This hook is called right after processing xml from GC. @param context.xml The xml message received from GC. @param context.response_xml The reply xml to GC. @return Should return nothing, because the return value is ignored. """ pass def on_message_receiving(self, context): """ This hook is called just before processing the received message from GC. @param context.message The message received from GC. @return Should return nothing, because the return value is ignored. """ pass def on_message_received(self, context): """ This hook is called right after processing message from GC. @param context.message The message received from GC. @param context.response_message The reply object to GC (either ok_t or error_t). @return Should return nothing, because the return value is ignored. """ pass def on_retrieve_order(self, order_id, context=None): """ This hook is called from message processing code just before calling the corresponding message handler. The idea is to allow user code to load order in one place and then receive the loaded object as parameter in message handler. This method should not throw if order is not found - instead it should return None. @param order_id The google order number corresponding to the message received. @return The order object that will be passed to message handlers. """ pass def handle_new_order(self, message, order_id, context, order=None): """ Google sends a new order notification when a buyer places an order through Google Checkout. Before shipping the items in an order, you should wait until you have also received the risk information notification for that order as well as the order state change notification informing you that the order's financial state has been updated to 'CHARGEABLE'. """ pass def handle_order_state_change(self, message, order_id, context, order=None): pass def handle_authorization_amount(self, message, order_id, context, order=None): pass def handle_risk_information(self, message, order_id, context, order=None): """ Google Checkout sends a risk information notification to provide financial information that helps you to ensure that an order is not fraudulent. """ pass def handle_charge_amount(self, message, order_id, context, order=None): pass def handle_refund_amount(self, message, order_id, context, order=None): pass def handle_chargeback_amount(self, message, order_id, context, order=None): pass def handle_notification(self, message, order_id, context, order=None): """ This handler is called when a message received from GC and when the more specific message handler was not found or returned None (which means it was not able to process the message). @param message The message from GC to be processed. @param order_id The google order number for which message is sent. @param order The object loaded by on_retrieve_order(order_id) or None. @return If message was processed successfully then return gmodel.ok_t(). If an error occured when proessing, then the method should return any other value (not-None). If the message is of unknown type or can't be processed by this handler then return None. """ # By default return None because we don' handle anything pass def on_exception(self, exception, context): """ By default simply rethrow the exception ignoring context. Could be used for loggin all the processing errors. @param exception The exception that was caught, of (sub)type GcheckyError. @param context The request context where the exception occured. """ raise exception def __call_handler(self, handler_name, context, *args, **kwargs): if hasattr(self, handler_name): try: handler = getattr(self, handler_name) return handler(context=context, *args, **kwargs) except Exception, e: error = "Exception in user handler '%s': %s" % (handler_name, e) raise HandlerError(message=error, context=context, origin=e) error="Unknown user handler: '%s'" % (handler_name,) raise HandlerError(message=error, context=context) def _send_xml(self, msg, context, diagnose): """ The helper method that submits an xml message to GC. """ context.diagnose = diagnose url = self.get_order_processing_url(diagnose) context.url = url import urllib2 req = urllib2.Request(url=url, data=msg) req.add_header('Authorization', 'Basic %s' % (b64encode('%s:%s' % (self.vendor_id, self.merchant_key)),)) req.add_header('Content-Type', ' application/xml; charset=UTF-8') req.add_header('Accept', ' application/xml; charset=UTF-8') try: self.__call_handler('on_xml_sending', context=context) response = urllib2.urlopen(req).read() self.__call_handler('on_xml_sent', context=context) return response except urllib2.HTTPError, e: error = e.fp.read() raise SystemError(message='Error in urllib2.urlopen: %s' % (error,), context=context, origin=e) def send_message(self, message, context=None, diagnose=False): if context is None: context = ControllerContext(outgoing=True) context.message = message context.diagnose = diagnose if isinstance(message, gmodel.abstract_order_t): context.order_id = message.google_order_number try: try: self.__call_handler('on_message_sending', context=context) message_xml = message.toxml() context.xml = message_xml except Exception, e: error = "Error converting message to xml: '%s'" % (unicode(e), ) raise DataError(message=error, context=context, origin=e) response_xml = self._send_xml(message_xml, context=context, diagnose=diagnose) context.response_xml = response_xml response = self.__process_message_result(response_xml, context=context) context.response_message = response self.__call_handler('on_message_sent', context=context) return response except GcheckyError, e: return self.on_exception(exception=e, context=context) def __process_message_result(self, response_xml, context): try: doc = gxml.Document.fromxml(response_xml) except Exception, e: error = "Error converting message to xml: '%s'" % (unicode(e), ) raise LibraryError(message=error, context=context, origin=e) if context.diagnose: # It has to be a 'diagnosis' response, otherwise... omg!.. panic!... if doc.__class__ != gmodel.diagnosis_t: error = "The response has to be of type diagnosis_t, not '%s'" % (doc.__class__,) raise LibraryError(message=error, context=context) return doc # If the response is 'ok' or 'bye' just return, because its good if doc.__class__ == gmodel.request_received_t: return doc if doc.__class__ == gmodel.bye_t: return doc # It's not 'ok' so it has to be 'error', otherwise it's an error if doc.__class__ != gmodel.error_t: error = "Unknown response type (expected error_t): '%s'" % (doc.__class__,) raise LibraryError(message=error, context=context) # 'error' - process it by throwing an exception with error/warning text msg = 'Error message from GCheckout API:\n%s' % (doc.error_message, ) if doc.warning_messages: tmp = '' for warning in doc.warning_messages: tmp += '\n%s' % (warning,) msg += ('Additional warnings:%s' % (tmp,)) raise DataError(message=msg, context=context) def hello(self): context = ControllerContext() doc = self.send_message(gmodel.hello_t(), context) if isinstance(doc, gxml.Document) and (doc.__class__ != gmodel.bye_t): error = "Expected but got %s" % (doc.__class__,) raise LibraryError(message=error, context=context, origin=e) def archive_order(self, order_id): self.send_message( gmodel.archive_order_t(google_order_number=order_id)) def unarchive_order(self, order_id): self.send_message( gmodel.unarchive_order_t(google_order_number=order_id)) def send_buyer_message(self, order_id, message): self.send_message(gmodel.send_buyer_message_t( google_order_number = order_id, message = message, send_email = True )) def add_merchant_order_number(self, order_id, merchant_order_number): self.send_message(gmodel.add_merchant_order_number_t( google_order_number = order_id, merchant_order_number = merchant_order_number )) def add_tracking_data(self, order_id, carrier, tracking_number): self.send_message(gmodel.add_tracking_data_t( google_order_number = order_id, tracking_data = gmodel.tracking_data_t(carrier = carrier, tracking_number = tracking_number) )) def charge_order(self, order_id, amount): self.send_message(gmodel.charge_order_t( google_order_number = order_id, amount = gmodel.price_t(value = amount, currency = self.currency) )) def refund_order(self, order_id, amount, reason, comment=None): self.send_message(gmodel.refund_order_t( google_order_number = order_id, amount = gmodel.price_t(value = amount, currency = self.currency), reason = reason, comment = comment or None )) def authorize_order(self, order_id): self.send_message(gmodel.authorize_order_t( google_order_number = order_id )) def cancel_order(self, order_id, reason, comment=None): self.send_message(gmodel.cancel_order_t( google_order_number = order_id, reason = reason, comment = comment or None )) def process_order(self, order_id): self.send_message(gmodel.process_order_t( google_order_number = order_id )) def deliver_order(self, order_id, carrier = None, tracking_number = None, send_email = None): tracking = None if carrier or tracking_number: tracking = gmodel.tracking_data_t(carrier = carrier, tracking_number = tracking_number) self.send_message(gmodel.deliver_order_t( google_order_number = order_id, tracking_data = tracking, send_email = send_email or None )) # This method gets a string and returns a string def receive_xml(self, input_xml, context=None): if context is None: context = ControllerContext(outgoing=False) context.xml = input_xml try: self.__call_handler('on_xml_receiving', context=context) try: input = gxml.Document.fromxml(input_xml) context.message = input except Exception, e: error = 'Error reading XML: %s' % (e,) raise DataError(message=error, context=context, origin=e) result = self.receive_message(message=input, order_id=input.google_order_number, context=context) context.response_message = result try: response_xml = result.toxml() context.response_xml = response_xml except Exception, e: error = 'Error reading XML: %s' % (e,) raise DataError(message=error, context=context, origin=e) self.__call_handler('on_xml_received', context=context) return response_xml except GcheckyError, e: return self.on_exception(exception=e, context=context) # A dictionary of document handler names. Comes handy in receive_message. __MESSAGE_HANDLERS = { gmodel.new_order_notification_t: 'handle_new_order', gmodel.order_state_change_notification_t: 'handle_order_state_change', gmodel.authorization_amount_notification_t: 'handle_authorization_amount', gmodel.risk_information_notification_t: 'handle_risk_information', gmodel.charge_amount_notification_t: 'handle_charge_amount', gmodel.refund_amount_notification_t: 'handle_refund_amount', gmodel.chargeback_amount_notification_t: 'handle_chargeback_amount', } def receive_message(self, message, order_id, context): context.order_id = order_id self.__call_handler('on_message_receiving', context=context) # retreive order instance from DB given the google order number order = self.__call_handler('on_retrieve_order', context=context, order_id=order_id) # handler = None result = None if self.__MESSAGE_HANDLERS.has_key(message.__class__): handler_name = self.__MESSAGE_HANDLERS[message.__class__] result = self.__call_handler(handler_name, message=message, order_id=order_id, context=context, order=order) if result is None: result = self.__call_handler('handle_notification', message=message, order_id=order_id, context=context, order=order) error = None if result is None: error = "Notification '%s' was not handled" % (message.__class__,) elif not (result.__class__ is gmodel.ok_t): try: error = unicode(result) except Exception, e: error = "Invalid value returned by handler '%s': %s" % (handler_name, e) raise HandlerError(message=error, context=context, origin=e) if error is not None: result = gmodel.error_t(serial_number = 'error', error_message=error) else: # TODO: Remove this after testing assert result.__class__ is gmodel.ok_t self.__call_handler('on_message_received', context=context) return result # Just an alias with a shorter name. Controller = ControllerLevel_2 if __name__ == "__main__": def run_doctests(): import doctest doctest.testmod() run_doctests()