PayzenService.py 12.7 KB
Newer Older
1 2 3 4
import zope
from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet, interfaces
from Products.ERP5Type.XMLObject import XMLObject
Łukasz Nowak's avatar
Łukasz Nowak committed
5 6
from Products.ERP5Type.Document import newTempDocument
import hashlib
7
from zLOG import LOG, WARNING
8
import datetime
Łukasz Nowak's avatar
Łukasz Nowak committed
9 10
import os
import time
11
from Products.DCWorkflow.DCWorkflow import ValidationFailed
12

Łukasz Nowak's avatar
Łukasz Nowak committed
13 14 15 16 17 18 19
present = False
tz = None
if 'TZ' in os.environ:
  present = True
  tz = os.environ['TZ']
os.environ['TZ'] = 'UTC'
time.tzset()
Łukasz Nowak's avatar
Łukasz Nowak committed
20 21 22 23 24 25
try:
  import suds
except ImportError:
  class PayzenSOAP:
    pass
else:
Łukasz Nowak's avatar
Łukasz Nowak committed
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
  def setUTCTimeZone(fn):
    def wrapped(*args, **kwargs):
      present = False
      tz = None
      if 'TZ' in os.environ:
        present = True
        tz = os.environ['TZ']
      os.environ['TZ'] = 'UTC'
      time.tzset()
      try:
        return fn(*args, **kwargs)
      finally:
        if present:
          os.environ['TZ'] = tz
        else:
          del(os.environ['TZ'])
        time.tzset()
    return wrapped
      
Łukasz Nowak's avatar
Łukasz Nowak committed
45
  class PayzenSOAP:
Łukasz Nowak's avatar
Łukasz Nowak committed
46
    """SOAP communication
47

Łukasz Nowak's avatar
Łukasz Nowak committed
48 49
    Methods are returning list of:
      * parsed response
50
      * signature check (True or False)
Łukasz Nowak's avatar
Łukasz Nowak committed
51 52
      * sent XML
      * received XML
53

Łukasz Nowak's avatar
Łukasz Nowak committed
54 55
    SOAP protocol is assumed as untrusted and dangerous, users of those methods
    are encouraged to log such messages for future debugging."""
Łukasz Nowak's avatar
Łukasz Nowak committed
56
    def _check_transactionInfoSignature(self, data):
57 58 59
      """Checks transactionInfo signature
      Can raise.
      """
60 61 62 63 64 65 66 67 68 69
      received_sorted_keys = ['errorCode', 'extendedErrorCode',
        'transactionStatus', 'shopId', 'paymentMethod', 'contractNumber',
        'orderId', 'orderInfo', 'orderInfo2', 'orderInfo3', 'transmissionDate',
        'transactionId', 'sequenceNb', 'amount', 'initialAmount', 'devise',
        'cvAmount', 'cvDevise', 'presentationDate', 'type', 'multiplePaiement',
        'ctxMode', 'cardNumber', 'cardNetwork', 'cardType', 'cardCountry',
        'cardExpirationDate', 'customerId', 'customerTitle', 'customerName',
        'customerPhone', 'customerMail', 'customerAddress', 'customerZipCode',
        'customerCity', 'customerCountry', 'customerLanguage', 'customerIP',
        'transactionCondition', 'vadsEnrolled', 'vadsStatus', 'vadsECI',
Łukasz Nowak's avatar
Łukasz Nowak committed
70
        'vadsXID', 'vadsCAVVAlgorithm', 'vadsCAVV', 'vadsSignatureValid',
71 72 73 74
        'directoryServer', 'authMode', 'markAmount', 'markDevise', 'markDate',
        'markNb', 'markResult', 'markCVV2_CVC2', 'authAmount', 'authDevise',
        'authDate', 'authNb', 'authResult', 'authCVV2_CVC2', 'warrantlyResult',
        'captureDate', 'captureNumber', 'rapprochementStatut', 'refoundAmount',
75
        'refundDevise', 'litige', 'timestamp']
Łukasz Nowak's avatar
Łukasz Nowak committed
76 77

      signature = self._getSignature(data, received_sorted_keys)
78 79
      return signature == data.signature

Łukasz Nowak's avatar
Łukasz Nowak committed
80
    @setUTCTimeZone
Łukasz Nowak's avatar
Łukasz Nowak committed
81
    def soap_getInfo(self, transmissionDate, transactionId):
Łukasz Nowak's avatar
Łukasz Nowak committed
82
      """Returns getInfo as dict, booelan, string, string
83

Łukasz Nowak's avatar
Łukasz Nowak committed
84
      transmissionDate is "raw" date in format YYYYMMDD, without any marks
Łukasz Nowak's avatar
Łukasz Nowak committed
85 86 87 88
      transactionId is id of transaction for this date

      As soon as communication happeneded does not raise.
      """
Łukasz Nowak's avatar
Łukasz Nowak committed
89
      client = suds.client.Client(self.wsdl_link.getUrlString())
Łukasz Nowak's avatar
Łukasz Nowak committed
90 91
      sorted_keys = ['shopId', 'transmissionDate', 'transactionId',
        'sequenceNb', 'ctxMode']
Łukasz Nowak's avatar
Łukasz Nowak committed
92 93 94 95 96
      kw = dict(
        transactionId=transactionId,
        ctxMode=self.getPayzenVadsCtxMode(),
        shopId=self.getServiceUsername(),
        sequenceNb=1,
Łukasz Nowak's avatar
Łukasz Nowak committed
97
        transmissionDate=transmissionDate,
Łukasz Nowak's avatar
Łukasz Nowak committed
98
      )
Łukasz Nowak's avatar
Łukasz Nowak committed
99
      kw['wsSignature'] = self._getSignature(kw, sorted_keys)
Łukasz Nowak's avatar
Łukasz Nowak committed
100

Łukasz Nowak's avatar
Łukasz Nowak committed
101
      data = client.service.getInfo(**kw)
Łukasz Nowak's avatar
Łukasz Nowak committed
102 103
      # Note: Code shall not raise since now, as communication begin and caller
      #       will have to log sent/received messages.
104
      try:
Łukasz Nowak's avatar
Łukasz Nowak committed
105 106 107 108 109 110 111 112 113 114 115 116
        data_kw = dict(data)
        for k in data_kw.keys():
          v = data_kw[k]
          if not isinstance(v, str):
            data_kw[k] = str(v)
      except Exception:
        data_kw = {}
        signature = False
        LOG('PayzenService', WARNING,
          'Issue during processing data_kw:', error=True)
      else:
        try:
Łukasz Nowak's avatar
Łukasz Nowak committed
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
          signature = self._check_transactionInfoSignature(data)
        except Exception:
          LOG('PayzenService', WARNING, 'Issue during signature calculation:',
            error=True)
          signature = False

      try:
        last_sent = str(client.last_sent())
      except Exception:
        LOG('PayzenService', WARNING,
          'Issue during converting last_sent to string:', error=True)
        signature = False

      try:
        last_received = str(client.last_received())
      except Exception:
        LOG('PayzenService', WARNING,
          'Issue during converting last_received to string:', error=True)
        signature = False

      return [data_kw, signature, last_sent, last_received]

Łukasz Nowak's avatar
Łukasz Nowak committed
139
    @setUTCTimeZone
Łukasz Nowak's avatar
Łukasz Nowak committed
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
    def soap_duplicate(self, transmissionDate, transactionId, presentationDate,
      newTransactionId, amount, devise, orderId='', orderInfo='', orderInfo2='',
      orderInfo3='', validationMode=0, comment=''):
      # prepare with passed parameters
      kw = dict(transmissionDate=transmissionDate, transactionId=transactionId,
        presentationDate=presentationDate, newTransactionId=newTransactionId,
        amount=amount, devise=devise, orderId=orderId, orderInfo=orderInfo,
        orderInfo2=orderInfo2, orderInfo3=orderInfo3,
        validationMode=validationMode, comment=comment)

      signature_sorted_key_list= ['shopId', 'transmissionDate', 'transactionId',
        'sequenceNb', 'ctxMode', 'orderId', 'orderInfo', 'orderInfo2',
        'orderInfo3', 'amount', 'devise', 'newTransactionId',
        'presentationDate', 'validationMode', 'comment']
      kw.update(
        ctxMode=self.getPayzenVadsCtxMode(),
        shopId=self.getServiceUsername(),
        sequenceNb=1,
      )
      kw['wsSignature'] = self._getSignature(kw, signature_sorted_key_list)
      # Note: Code shall not raise since now, as communication begin and caller
      #       will have to log sent/received messages.
      client = suds.client.Client(self.wsdl_link.getUrlString())
      data = client.service.duplicate(**kw)
      # Note: Code shall not raise since now, as communication begin and caller
      #       will have to log sent/received messages.
      try:
        data_kw = dict(data)
        for k in data_kw.keys():
          v = data_kw[k]
          if not isinstance(v, str):
            data_kw[k] = str(v)
      except Exception:
        data_kw = {}
        signature = False
        LOG('PayzenService', WARNING,
          'Issue during processing data_kw:', error=True)
Łukasz Nowak's avatar
Łukasz Nowak committed
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
      else:
        try:
          signature = self._check_transactionInfoSignature(data)
        except Exception:
          LOG('PayzenService', WARNING, 'Issue during signature calculation:',
            error=True)
          signature = False

      try:
        last_sent = str(client.last_sent())
      except Exception:
        LOG('PayzenService', WARNING,
          'Issue during converting last_sent to string:', error=True)
        signature = False

      try:
        last_received = str(client.last_received())
      except Exception:
        LOG('PayzenService', WARNING,
          'Issue during converting last_received to string:', error=True)
        signature = False

      return [data_kw, signature, last_sent, last_received]

    @setUTCTimeZone
    def soap_cancel(self, transmissionDate, transactionId, comment=''):
      # prepare with passed parameters
      kw = dict(transmissionDate=transmissionDate, transactionId=transactionId,
        comment=comment)

      signature_sorted_key_list= ['shopId', 'transmissionDate', 'transactionId',
        'sequenceNb', 'ctxMode', 'comment']
      kw.update(
        ctxMode=self.getPayzenVadsCtxMode(),
        shopId=self.getServiceUsername(),
        sequenceNb=1,
      )
      kw['wsSignature'] = self._getSignature(kw, signature_sorted_key_list)
      # Note: Code shall not raise since now, as communication begin and caller
      #       will have to log sent/received messages.
      client = suds.client.Client(self.wsdl_link.getUrlString())
      data = client.service.cancel(**kw)
      # Note: Code shall not raise since now, as communication begin and caller
      #       will have to log sent/received messages.
      try:
        data_kw = dict(data)
        for k in data_kw.keys():
          v = data_kw[k]
          if not isinstance(v, str):
            data_kw[k] = str(v)
      except Exception:
        data_kw = {}
        signature = False
        LOG('PayzenService', WARNING,
          'Issue during processing data_kw:', error=True)
Łukasz Nowak's avatar
Łukasz Nowak committed
232 233 234
      else:
        try:
          signature = self._check_transactionInfoSignature(data)
Łukasz Nowak's avatar
Łukasz Nowak committed
235 236 237 238 239 240 241
        except Exception:
          LOG('PayzenService', WARNING, 'Issue during signature calculation:',
            error=True)
          signature = False

      try:
        last_sent = str(client.last_sent())
242
      except Exception:
Łukasz Nowak's avatar
Łukasz Nowak committed
243 244
        LOG('PayzenService', WARNING,
          'Issue during converting last_sent to string:', error=True)
245
        signature = False
Łukasz Nowak's avatar
Łukasz Nowak committed
246 247 248 249 250 251 252 253 254

      try:
        last_received = str(client.last_received())
      except Exception:
        LOG('PayzenService', WARNING,
          'Issue during converting last_received to string:', error=True)
        signature = False

      return [data_kw, signature, last_sent, last_received]
Łukasz Nowak's avatar
Łukasz Nowak committed
255 256 257 258 259 260
finally:
  if present:
    os.environ['TZ'] = tz
  else:
    del(os.environ['TZ'])
  time.tzset()
Łukasz Nowak's avatar
Łukasz Nowak committed
261 262

class PayzenService(XMLObject, PayzenSOAP):
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
  meta_type = 'Payzen Service'
  portal_type = 'Payzen Service'

  zope.interface.implements(interfaces.IPaymentService)

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.XMLObject
                    , PropertySheet.Reference
                    )
  def initialize(self, REQUEST=None, **kw):
    """See Payment Service Interface Documentation"""
    pass

Łukasz Nowak's avatar
Łukasz Nowak committed
281
  def _getSignature(self, ob, sorted_key_list):
Łukasz Nowak's avatar
Łukasz Nowak committed
282 283 284 285 286 287 288 289 290 291 292 293
    """Calculates signature from ob

    ob can be dict or getattr capable object

    in case if ob is a dict all .strftime callable values
    are converted to datetime soapish format
    """
    if isinstance(ob, dict):
      isdict = True
    else:
      isdict = False

Łukasz Nowak's avatar
Łukasz Nowak committed
294
    signature = ''
Łukasz Nowak's avatar
Łukasz Nowak committed
295 296 297 298 299 300 301 302
    for k in sorted_key_list:
      if isdict:
        v = ob[k]
      else:
        v = getattr(ob, k, None)
      if v is None:
        # empty or not transmitted -- add as empty
        v = ''
Łukasz Nowak's avatar
Łukasz Nowak committed
303
      elif isinstance(v, datetime.datetime):
Łukasz Nowak's avatar
Łukasz Nowak committed
304
        # for sure date
Łukasz Nowak's avatar
Łukasz Nowak committed
305
        v = v.strftime('%Y%m%d')
Łukasz Nowak's avatar
Łukasz Nowak committed
306
#        import ipdb ; ipdb.set_trace()
Łukasz Nowak's avatar
Łukasz Nowak committed
307 308 309
      else:
        # anything else cast to string
        v = str(v)
Łukasz Nowak's avatar
Łukasz Nowak committed
310 311 312 313 314
      signature += v + '+'
    signature += self.getServicePassword()
    return hashlib.sha1(signature).hexdigest()

  def _getFieldList(self, payzen_dict):
Łukasz Nowak's avatar
Łukasz Nowak committed
315 316 317 318 319 320 321 322 323
    payzen_dict.update(
      vads_action_mode=self.getPayzenVadsActionMode(),
      vads_ctx_mode=self.getPayzenVadsCtxMode(),
      vads_contrib='ERP5',
      vads_page_action=self.getPayzenVadsPageAction(),
      vads_payment_config='SINGLE',
      vads_site_id=self.getServiceUsername(),
      vads_version=self.getPayzenVadsVersion()
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
324
    # fetch all prepared vads_ values and remove them from dict
Łukasz Nowak's avatar
Łukasz Nowak committed
325
    signature = self._getSignature(payzen_dict, sorted(payzen_dict.keys()))
Łukasz Nowak's avatar
Łukasz Nowak committed
326 327
    payzen_dict['signature'] = signature
    field_list = []
Łukasz Nowak's avatar
Łukasz Nowak committed
328 329 330 331 332 333
    for k,v in payzen_dict.iteritems():
      field_list.append((k, v))
    return field_list

  def navigate(self, page_template, payzen_dict, REQUEST=None, **kw):
    """Returns configured template used to do the payment"""
334 335 336 337 338 339 340 341 342 343 344
    check_result = self.checkConsistency()
    message_list = []
    for err in check_result:
      if getattr(err, 'getTranslatedMessage', None) is not None:
        message_list.append(err.getTranslatedMessage())
      else:
        # backward compatibility:
        message_list.append(err[3])
    if message_list:
      raise ValidationFailed, message_list

Łukasz Nowak's avatar
Łukasz Nowak committed
345
    temp_document = newTempDocument(self, 'id')
Łukasz Nowak's avatar
Łukasz Nowak committed
346 347 348
    temp_document.edit(
      link_url_string=self.getLinkUrlString(),
      title='title',
Łukasz Nowak's avatar
Łukasz Nowak committed
349
      field_list=self._getFieldList(payzen_dict),
Łukasz Nowak's avatar
Łukasz Nowak committed
350 351 352 353
      # append the rest of transmitted parameters page template
      **kw
    )
    return getattr(temp_document, page_template)()
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368

  def notifySuccess(self, REQUEST=None, **kw):
    """See Payment Service Interface Documentation"""
    raise NotImplementedError
    return self._getTypeBasedMethod("acceptPayment")(**kw)

  def notifyFail(self, REQUEST=None, **kw):
    """See Payment Service Interface Documentation"""
    raise NotImplementedError
    return self._getTypeBasedMethod("failInPayment")(**kw)

  def notifyCancel(self, REQUEST=None, **kw):
    """See Payment Service Interface Documentation"""
    raise NotImplementedError
    return self._getTypeBasedMethod("abortPayment")(**kw)