gchecky.controller

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

# Source Code for Module gchecky.controller

      1  from base64 import b64encode
      2
      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" border="0" alt="Google Checkout" />
     30                 </form>
     31                 """ % (self.url, self.cart, self.signature, 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):
    119          html = self.prepare_order(order, order_id, diagnose)
    120          html.url = self.get_client_donation_url(diagnose)
    121          html.button = self.get_donation_button_url(diagnose)
    122          return html
    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):
    469          self.send_message(
    470              gmodel.archive_order_t(google_order_number=order_id))
    471
    472 -    def unarchive_order(self, order_id):
    473          self.send_message(
    474              gmodel.unarchive_order_t(google_order_number=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):
    484          self.send_message(gmodel.add_merchant_order_number_t(
    485              google_order_number = order_id,
    486              merchant_order_number = merchant_order_number
    487              ))
    488
    489 -    def add_tracking_data(self, order_id, carrier, tracking_number):
    490          self.send_message(gmodel.add_tracking_data_t(
    491              google_order_number = order_id,
    492              tracking_data = gmodel.tracking_data_t(carrier         = carrier,
    493                                                     tracking_number = tracking_number)
    494              ))
    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):
    511          self.send_message(gmodel.authorize_order_t(
    512              google_order_number = order_id
    513          ))
    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):
    523          self.send_message(gmodel.process_order_t(
    524              google_order_number = order_id
    525              ))
    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