add local test server and refactor
authorNicolas Chauvat <nicolas.chauvat@logilab.fr>
Tue, 22 Jun 2010 22:14:44 +0200
changeset 2 80232b459741
parent 1 bbea5dfcadcb
child 3 b81e32bb48d6
add local test server and refactor
cmcic.py
localtestserver.py
views.py
--- a/cmcic.py	Mon Jun 21 20:27:00 2010 +0200
+++ b/cmcic.py	Tue Jun 22 22:14:44 2010 +0200
@@ -24,26 +24,23 @@
 
 .. sourcecode:: python
 
-    import cmcic, ConfigParser
+    import cmcic
 
     # initialize terminal
-    cfg = ConfigParser.RawConfigParser()
-    cfg.read('mytpe')
-    tpe = cmcic.PaymentTerminal(cfg)
+    tpe = cmcic.get_tpe('mytpe')
 
-    # build transaction
-    transac = cmcic.Transaction()
-    transac.reference = "ref" + datetime.datetime.now().strftime("%H%M%S");
-    transac.amount = "1.01"
-    transac.currency = "EUR"
-    transac.description = "Some description"
-    transac.date = datetime.datetime.now().strftime("%d/%m/%Y:%H:%M:%S")
-    transac.lang = "FR"
-    transac.email = "test@test.zz"
-    errors = transac.has_errors()
-    if errors:
-        print errors
-        sys.exit(1)
+    # build payment request
+    payreq = cmcic.PaymentRequest()
+    payreq.reference = "ref" + datetime.datetime.now().strftime("%H%M%S");
+    payreq.amount = "1.01"
+    payreq.currency = "EUR"
+    payreq.description = "Some description"
+    payreq.date = datetime.datetime.now().strftime("%d/%m/%Y:%H:%M:%S")
+    payreq.lang = "FR"
+    payreq.email = "test@test.zz"
+    payreq.url_root = 'http://www.toto.com/'
+    payreq.url_ok = 'http://www.toto.com/cart/1234'
+    payreq.url_err = 'http://www.toto.com/cart/1234'
 
     # generate html
     print '''
@@ -62,8 +59,8 @@
     </body>
     </html>
     ''' % (tpe.id_str(),
-           tpe.mk_transaction_msg(transac),
-           cmcic.html_form(tpe, transac))
+           tpe.paymentrequest_msg(payreq)[1],
+           cmcic.html_form(tpe, payreq))
 
 Acknowledge payment
 ===================
@@ -73,36 +70,33 @@
 
 .. sourcecode:: python
 
-    import cmcic, cgi, ConfigParser
+    import cmcic
 
     # initialize terminal
-    cfg = ConfigParser.RawConfigParser()
-    cfg.read('mytpe')
-    tpe = cmcic.PaymentTerminal(cfg)
+    tpe = cmcic.get_tpe('mytpe')
 
     # handle response
-    cert = cmcic.Certification(cgi.FieldStorage())
-    if tpe.is_valid_cert(cert):
-        if cert.return_code == "Annulation":
+    payrep = tpe.read_paymentresponse(cgi.FieldStorage())
+    msg, mac = tpe.paymentresponse_msg(payrep)
+    if tpe.is_valid_msg(msg, mac):
+        if payrep.return_code == "Annulation":
             # Payment has been refused
             # The payment may be accepted later
-            # put your code here (email sending / Database update)
             pass
 
-        elif cert.return_code in ("payetest","paiement"):
+        elif payrep.return_code in ("payetest","paiement"):
             # Payment has been accepeted on the productive server
-            # put your code here (email sending / Database update)
             pass
 
         #*** ONLY FOR MULTIPART PAYMENT ***#
-        elif cert.return_code.startswith("paiement_pf"):
+        elif payrep.return_code.startswith("paiement_pf"):
             # Payment has been accepted on the productive server for the part #N
             # return code is like paiement_pf[#N]
             # put your code here (email sending / Database update)
             # You have the amount of the payment part in Certification['montantech']
             pass
 
-        elif cert.return_code.startswith("Annulation_pf"):
+        elif payrep.return_code.startswith("Annulation_pf"):
             # Payment has been refused on the productive server for the part #N
             # return code is like Annulation_pf[#N]
             # put your code here (email sending / Database update)
@@ -112,7 +106,7 @@
         sResult = "0"
     else :
         # your code if the HMAC doesn't match
-        sResult = "1\n" + tpe.mk_certification_msg(cert)
+        sResult = "1\n" + mac
 
     #-----------------------------------------------------------------------------
     # Send receipt to CMCIC server
@@ -122,9 +116,32 @@
 """
 __docformat__ = 'restructuredtext'
 
-import sys, hmac, sha
+import hmac, sha, os.path, ConfigParser
+
+def dict_translate(msg, map):
+    result = {}
+    for key, value in msg.items():
+        key = map.get(key, key)
+        if key is not None:
+            result[key] = value
+    return result
+
+class NamedTuple(object):
+    __slots__ = ()
 
-class Transaction(object):
+    def __init__(self, **kwargs):
+        for attr in self.__slots__:
+            setattr(self, attr, kwargs.pop(attr, None))
+        if kwargs:
+            raise NameError('No attributes named %s' % repr(kwargs))
+
+    def as_dict(self):
+        result = {}
+        for attr in self.__slots__:
+            result[attr] = getattr(self, attr)
+        return result
+
+class PaymentRequest(NamedTuple):
     """
     reference  : unique, alphaNum (A-Z a-z 0-9), 12 characters max
     amount     : format  "xxxxx.yy" (no spaces)
@@ -133,71 +150,42 @@
     date       : format dd/mm/yyyy:hh:mm:ss
     email      : buyer's email
     payments   : amount must be paid in at most 4 payments [(date1, amount1), ...]
+    options    : ...
     """
-
-    def __init__(self):
-        self.reference = None
-        self.amount = None
-        self.currency = None
-        self.description = None
-        self.date = None
-        self.email = None
-        self.payments = []
-        self.options = None
+    __slots__ = ('mac reference amount currency description date email payments '
+                 'options url_root url_ok url_err lang').split()
 
-    def has_errors(self):
-        errors = []
-        if not self.reference:
-            errors.append('reference is missing')
-        if not self.amount:
-            errors.append('amount is missing')
-        if not self.currency:
-            errors.append('currency is missing')
-        if not self.date:
-            errors.append('date is missing')
-        if not self.email:
-            errors.append('email is missing')
-        if self.payments and sum(item[1] for item in payments) != self.amount:
-            errors.append('sum of payments != amount')
-        return errors
+class PaymentResponse(NamedTuple):
+    __slots__ = ('mac reference amount date description return_code cvx vld '
+                 'brand status3ds numauto motifrefus originecb bincb hpancb '
+                 'ipclient originetr veres pares montantech').split()
+
+RESPONSE_TRANSLATION = dict([
+    ('MAC','mac'), ('montant','amount'), ('texte-libre','description'),
+    ('code-retour','return_code'),
+    ])
 
-CERT_MAP = [('MAC','mac'), ('date','date'), ('montant','amount'),
-            ('reference','reference'), ('texte-libre','description'),
-            ('code-retour','return_code'), ('cvx','cvx'), ('vld','vld'),
-            ('brand','brand'), ('status3ds', 'status3ds'),
-            ('numauto','numauto'), ('motifrefus','motifrefus'),
-            ('originecb','originecb'), ('bincb','bincb'), ('hpancb', 'hpancb'),
-            ('ipclient', 'ipclient'), ('originetr', 'originetr'),
-            ('veres', 'veres'), ('pares','pares'), ('montantech', 'montantech'),
-            ]
+REQUEST_TRANSLATION = dict([
+    ('MAC','mac'), ('montant', 'amount'), ('lgue','lang'),
+    ('url_retour', 'url_root'), ('texte-libre', 'description'),
+    ('url_retour_err', 'url_err'), ('url_retour_ok', 'url_ok'),
+    ('version', None), ('societe', None), ('TPE', None), ('bouton', None),
+    ('mail', 'email'),
+    ])
 
-class Certification(object):
-
-    def __init__(self, params):
-        for src, dst in CERT_MAP:
-            if params.has_key(src):
-                setattr(self, dst, params[src].value)
-            else:
-                setattr(self, dst, None)
+class PaymentProtocol(object):
 
-class PaymentTerminal(object):
-
-    URL_SERVEUR = "https://paiement.creditmutuel.fr/paiement.cgi"
-    URL_SERVEUR_TEST = "https://paiement.creditmutuel.fr/test/paiement.cgi"
-
-    def __init__(self, cfg, test=True) :
+    def __init__(self, cfg) :
         self.version = cfg.get('cmcic_tpe', 'version')
-        self._cle = cfg.get('cmcic_tpe', 'key')
-        self.numero = cfg.get('cmcic_tpe', 'number')
-        self.code_societe = cfg.get('cmcic_tpe', 'company')
-        self.url_paiement = test and self.URL_SERVEUR_TEST or self.URL_SERVEUR
-        self.url_ok = 'http://www.google.fr/'
-        self.url_ko = 'http://www.google.de/'
-        self._usablekey = self.getUsableKey()
+        self.tpe_key = self.decode_key(cfg.get('cmcic_tpe', 'key'))
+        self.tpe_number = cfg.get('cmcic_tpe', 'number')
+        self.tpe_company = cfg.get('cmcic_tpe', 'company')
+        self.server_url = cfg.get('cmcic_tpe', 'server_url')
+        self.return_url = cfg.get('cmcic_tpe', 'return_url')
 
-    def getUsableKey(self):
-        hexStrKey  = self._cle[0:38]
-        hexFinal   = self._cle[38:40] + "00"
+    def decode_key(self, key):
+        hexStrKey = key[0:38]
+        hexFinal = key[38:40] + "00"
         cca0 = ord(hexFinal[0:1])
         if cca0 > 70 and cca0 < 97:
             hexStrKey += chr(cca0-23) + hexFinal[1:2]
@@ -211,63 +199,87 @@
         return hexStrKey
 
     def compute_hmac(self, data):
-        hash = hmac.HMAC(self._usablekey, None, sha)
+        hash = hmac.HMAC(self.tpe_key, None, sha)
         hash.update(data)
         return hash.hexdigest()
 
-    def is_valid_hmac(self, data, mac):
-        return self.compute_hmac(data) == mac.lower()
-
     def id_str(self):
-        msg = "CtlHmac%s%s" % (self.version, self.numero)
+        msg = "CtlHmac%s%s" % (self.version, self.tpe_number)
         return "V1.04.sha1.py--[%s]-%s" % (msg, self.compute_hmac(msg))
 
-    def mk_transaction_msg(self, transac):
-        items = [self.numero, transac.date, transac.amount+transac.currency,
-                 transac.reference, transac.description, self.version,
-                 transac.lang, self.code_societe, transac.email]
-        if len(transac.payments):
-            items.append(len(transac.payments))
+    def read_paymentrequest(self, params):
+        params = dict_translate(params, REQUEST_TRANSLATION)
+        req = PaymentRequest(**params)
+        return req
+
+    def read_paymentresponse(self, params):
+        params = dict_translate(params, RESPONSE_TRANSLATION)
+        req = PaymentResponse(**params)
+        return req
+
+    def paymentrequest_msg(self, req):
+        items = [self.tpe_number, req.date, req.amount+req.currency,
+                 req.reference, req.description, self.version,
+                 req.lang, self.tpe_company, req.email]
+        payments = req.payments or []
+        if len(payments):
+            items.append(len(payments))
         else:
             items.append('')
-        for date, amount in transac.payments:
+        for date, amount in payments:
             items.append(date)
-            items.append(amount+transac.currency)
-        for i in range(len(transac.payments),4):
+            items.append(amount+req.currency)
+        for i in range(len(payments),4):
             items.append('')
             items.append('')
-        items.append(transac.options or '')
+        items.append(req.options or '')
         items = [str(item) for item in items]
-        return '*'.join(items)
+        msg = '*'.join(items)
+        hmac = self.compute_hmac(msg)
+        return msg, hmac
 
-    def mk_certification_msg(self, cert):
-        items = [self.numero, cert.date, cert.amount, cert.reference, cert.description,
-                 self.version, cert.return_code, cert.cvx, cert.vld, cert.brand, cert.status3ds,
-                 cert.numauto, cert.motifrefus, cert.originecb, cert.bincd, cert.hpancb,
-                 cert.ipclient, cert.originetr, cert.veres, cert.pares, '']
-        return '*'.join(items)
-
-    def is_valid_cert(self, cert):
-        return self.is_valid_hmac(self.mk_certification_msg(cert), cert.mac)
+    def paymentresponse_msg(self, rep):
+        items = []
+        for item in [self.tpe_number, rep.date, rep.amount, rep.reference, rep.description,
+                     rep.return_code, rep.cvx, rep.vld, rep.brand, rep.status3ds,
+                     rep.numauto, rep.motifrefus, rep.originecb, rep.bincb, rep.hpancb,
+                     rep.ipclient, rep.originetr, rep.veres, rep.pares, '']:
+            if item is None:
+                items.append('')
+            else:
+                items.append(item)
+        msg = '*'.join(items)
+        hmac = self.compute_hmac(msg)
+        return msg, hmac
 
-def html_form(tpe, transac):
-    form = ['<form action="%s" method="post" id="PaymentRequest">' % tpe.url_paiement]
+    def is_valid_msg(self, msg, mac):
+        return self.compute_hmac(msg) == mac.lower()
+
+def html_form(tpe, req, label):
+    form = [u'<form action="%s" method="post" id="PaymentRequest">' % tpe.server_url]
+    msg, mac = tpe.paymentrequest_msg(req)
     fields = [("version"         ,tpe.version),
-              ("TPE"             ,tpe.numero),
-              ("date"            ,transac.date),
-              ("montant"         ,transac.amount + transac.currency),
-              ("reference"       ,transac.reference),
-              ("MAC"             ,tpe.compute_hmac(tpe.mk_transaction_msg(transac))),
-              ("url_retour"      ,tpe.url_ko),
-              ("url_retour_ok"   ,tpe.url_ok),
-              ("url_retour_err"  ,tpe.url_ko),
-              ("lgue"            ,transac.lang),
-              ("societe"         ,tpe.code_societe),
-              ("texte-libre"     ,transac.description),
-              ("mail"            ,transac.email),
+              ("TPE"             ,tpe.tpe_number),
+              ("date"            ,req.date),
+              ("montant"         ,req.amount + req.currency),
+              ("reference"       ,req.reference),
+              ("MAC"             ,mac),
+              ("url_retour"      ,req.url_root),
+              ("url_retour_ok"   ,req.url_ok),
+              ("url_retour_err"  ,req.url_err),
+              ("lgue"            ,req.lang),
+              ("societe"         ,tpe.tpe_company),
+              ("texte-libre"     ,req.description),
+              ("mail"            ,req.email),
               ]
     for name, value in fields:
-	form.append('<input type="hidden" name="%s" id="%s" value="%s" />' % (name, name, value))
-    form.append('<input type="submit" name="bouton" id="bouton" value="Connexion / Connection" />')
-    form.append('</form>')
-    return ''.join(form)
+	form.append(u'<input type="hidden" name="%s" id="%s" value="%s" />' % (name, name, value))
+    form.append(u'<input type="submit" name="bouton" id="bouton" value="%s" />' % label)
+    form.append(u'</form>')
+    return u''.join(form)
+
+def get_tpe(cfgpath):
+    cfg = ConfigParser.RawConfigParser()
+    cfg.read(cfgpath)
+    return PaymentProtocol(cfg)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/localtestserver.py	Tue Jun 22 22:14:44 2010 +0200
@@ -0,0 +1,109 @@
+"""Local test server for cmcic payment system.
+
+Start it with:
+
+./localtestserver.py [accept|reject] path/to/tpe.cfg
+
+First argument is action in response to payment request: accept or reject.
+Second argument is full path to tpe config file.
+"""
+
+import sys, cgi, BaseHTTPServer, urlparse, urllib, urllib2
+
+index_html = '''<h1>Local test server for CM-CIC paiement</h1>
+<p>%sing payment requests for TPE %s (%s)</p>
+'''
+
+pay_html = '''<p>Received payment request:</p>
+%s
+<p>Sending response:</p>
+%s
+<p>Response sent to %s and and got return value %s.</p>
+'''
+
+def dict_as_html_table(map):
+    html = '<table>'
+    for key in sorted(map):
+        html += '<tr><td>%s</td><td>%s</td></tr>' % (repr(key), repr(map[key]))
+    html += '</table>'
+    return html
+
+class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+    def _read_form(self):
+        ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
+        length = int(self.headers.getheader('content-length'))
+        if ctype == 'multipart/form-data':
+            self.form = cgi.parse_multipart(self.rfile, pdict)
+        elif ctype == 'application/x-www-form-urlencoded':
+            query = self.rfile.read(length)
+            self.form = cgi.parse_qs(query, keep_blank_values=1)
+        else:
+            self.form = {}
+
+    def do_GET(self):
+        if self.path == '/':
+            body = index_html % (self.server.action,
+                                 self.server.tpe.tpe_company,
+                                 self.server.tpe.tpe_number)
+            self.send_response(200)
+            self.send_header("Content-type", 'text/html')
+            self.end_headers()
+            self.wfile.write(body)
+        else:
+            self.send_response(404,'Not found')
+
+    def do_POST(self):
+        self._read_form()
+        if self.path.startswith('/paiement.cgi'):
+            req = self.read_paymentrequest()
+            rep = self.build_paymentresponse(req)
+            ack = self.send_paymentresponse(rep)
+            body = index_html % (self.server.action,
+                                 self.server.tpe.tpe_company,
+                                 self.server.tpe.tpe_number)
+            body += pay_html % (dict_as_html_table(req.as_dict()),
+                                dict_as_html_table(rep.as_dict()),
+                                self.server.tpe.return_url,
+                                ack)
+            self.send_response(200)
+            self.send_header("Content-type", 'text/html')
+            self.end_headers()
+            self.wfile.write(body)
+        else:
+            self.send_response(404, 'Not found')
+
+    def read_paymentrequest(self):
+        params = dict([(key,value[0]) for key,value in self.form.items()])
+        return self.server.tpe.read_paymentrequest(params)
+
+    def build_paymentresponse(self, req):
+        rep = cmcic.PaymentResponse()
+        for attr in 'reference amount date description'.split():
+            setattr(rep, attr, getattr(req, attr))
+        if self.server.action == 'accept':
+            rep.return_code = 'payetest'
+        elif self.server.action == 'reject':
+            rep.return_code = 'Annulation'
+        msg, mac = self.server.tpe.paymentresponse_msg(rep)
+        rep.mac = mac
+        return rep
+
+    def send_paymentresponse(self, rep):
+        try:
+            ack = urllib2.urlopen(self.server.tpe.return_url, urllib.urlencode(rep.as_dict())).read()
+        except urllib2.HTTPError, exc:
+            ack = str(exc)
+        return ack
+
+if __name__ == '__main__':
+    import cmcic
+    action = sys.argv[1]
+    cfg = sys.argv[2]
+    tpe = cmcic.get_tpe(cfg)
+    port = int(urlparse.urlparse(tpe.server_url)[1].split(':')[1])
+    httpd = BaseHTTPServer.HTTPServer(('', port), RequestHandler)
+    httpd.tpe = tpe
+    httpd.action = action
+    httpd.serve_forever()
+
--- a/views.py	Mon Jun 21 20:27:00 2010 +0200
+++ b/views.py	Tue Jun 22 22:14:44 2010 +0200
@@ -15,3 +15,97 @@
 # with this program. If not, see <http://www.gnu.org/licenses/>.
 
 """cubicweb-cmcicpay views/forms/actions/components for web ui"""
+
+import os
+
+from cubicweb.web import controller
+from cubicweb.selectors import match_user_groups
+from cubicweb.view import StartupView
+from cubicweb.web.views.urlrewrite import SimpleReqRewriter, rgx
+
+from cubes.cmcicpay import cmcic
+
+# class CmcicController(controller.Controller):
+#     __regid__ = 'cmcic'
+
+#     def publish(self, rset=None):
+#         self.w(u'<h1>CM-CIC p@iement</h1>')
+#         tpe = cmcic.get_tpe(self._cw)
+#         self.w(u'<table>')
+#         for attr in ['numero','version','code_societe','url_paiement','url_ok','url_ko']:
+#             self.w(u'<tr><td>%s</td><td>%s</td></tr>' % (attr, getattr(tpe, attr)))
+#         self.w(u'</table>')
+#         self.w(repr(self._cw.form))
+
+
+def get_tpe(_cw):
+    return cmcic.get_tpe(os.path.join(_cw.vreg.config.apphome,'tpe'))
+
+## urls
+
+class ConfRewrite(SimpleReqRewriter):
+    rules = [
+        (rgx('/cmcic'), dict(vid='cmcic')),
+        (rgx('/cmcic_info'), dict(vid='cmcic_info')),
+        ]
+## views
+
+class CmcicView(StartupView):
+    __regid__ = 'cmcic'
+    templatable = False
+    content_type = 'text/plain'
+
+    def call(self):
+        #print "Pragma: no-cache'
+        tpe = get_tpe(self._cw)
+        params = dict(self._cw.form)
+        del params['vid']
+        rep = tpe.read_paymentresponse(params)
+        msg, mac = tpe.paymentresponse_msg(rep)
+        if tpe.is_valid_msg(msg, mac):
+            if rep.return_code == "Annulation":
+                # Payment has been refused
+                # The payment may be accepted later
+                # put your code here (email sending / Database update)
+                print 'cmcic', rep.reference, rep.return_code
+
+            elif rep.return_code in ("payetest","paiement"):
+                # Payment has been accepeted on the productive server
+                # put your code here (email sending / Database update)
+                print 'cmcic', rep.reference, rep.return_code
+                cart = self._cw.entity_from_eid(rep.reference)
+                text = u'payed by %(brand)s on %(date)s, auth %(numauto)s' % rep.as_dict()
+                cart.fire_transition('check out', comment=text)
+
+            #*** ONLY FOR MULTIPART PAYMENT ***#
+            elif rep.return_code.startswith("paiement_pf"):
+                # Payment has been accepted on the productive server for the part #N
+                # return code is like paiement_pf[#N]
+                # put your code here (email sending / Database update)
+                # You have the amount of the payment part in Rpeification['montantech']
+                print 'cmcic', rep.reference, rep.return_code
+
+            elif rep.return_code.startswith("Annulation_pf"):
+                # Payment has been refused on the productive server for the part #N
+                # return code is like Annulation_pf[#N]
+                # put your code here (email sending / Database update)
+                # You have the amount of the payment part in Repification['montantech']
+                print 'cmcic', rep.reference, rep.return_code
+            ack = 0
+        else :
+            print "cmcic: the HMAC doesn't match."
+            ack = 1 #\n" + mac
+        self.w(u'version=%s\ncdr=%s' % (tpe.version, ack))
+
+class CmcicInfoView(StartupView):
+    __regid__ = 'cmcic_info'
+    title = _('CMCIC paiement')
+    __select__ = StartupView.__select__ & match_user_groups('managers')
+
+    def call(self):
+        self.w(u'<h1>CM-CIC p@iement</h1>')
+        tpe = get_tpe(self._cw)
+        self.w(u'<table>')
+        for attr in ['numero','version','code_societe','url_paiement','url_ok','url_ko']:
+            self.w(u'<tr><td>%s</td><td>%s</td></tr>' % (attr, getattr(tpe, attr)))
+        self.w(u'</table>')