Commit ceaef5a3 authored by Rafael Monnerat's avatar Rafael Monnerat

erp5_payzen_secure_payment: Move Payzen to REST API

See merge request nexedi/erp5!1326
parents 36477a37 d6dcd5a4
Pipeline #13046 running with stage
......@@ -4,9 +4,11 @@ from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5Type.XMLObject import XMLObject
import hashlib
from zLOG import LOG, WARNING
import base64
import datetime
import os
import time
import requests
from Products.DCWorkflow.DCWorkflow import ValidationFailed
present = False
......@@ -16,250 +18,79 @@ if 'TZ' in os.environ:
tz = os.environ['TZ']
os.environ['TZ'] = 'UTC'
time.tzset()
try:
import suds
except ImportError:
class PayzenSOAP:
pass
else:
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
class PayzenSOAP:
"""SOAP communication
Methods are returning list of:
* parsed response
* signature check (True or False)
* sent XML
* received XML
SOAP protocol is assumed as untrusted and dangerous, users of those methods
are encouraged to log such messages for future debugging."""
def _check_transactionInfoSignature(self, data):
"""Checks transactionInfo signature
Can raise.
"""
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',
'vadsXID', 'vadsCAVVAlgorithm', 'vadsCAVV', 'vadsSignatureValid',
'directoryServer', 'authMode', 'markAmount', 'markDevise', 'markDate',
'markNb', 'markResult', 'markCVV2_CVC2', 'authAmount', 'authDevise',
'authDate', 'authNb', 'authResult', 'authCVV2_CVC2', 'warrantlyResult',
'captureDate', 'captureNumber', 'rapprochementStatut', 'refoundAmount',
'refundDevise', 'litige', 'timestamp']
signature = self._getSignature(data, received_sorted_keys)
return signature == data.signature
@setUTCTimeZone
def soap_getInfo(self, transmissionDate, transactionId):
"""Returns getInfo as dict, booelan, string, string
transmissionDate is "raw" date in format YYYYMMDD, without any marks
transactionId is id of transaction for this date
As soon as communication happeneded does not raise.
"""
client = suds.client.Client(self.wsdl_link.getUrlString())
sorted_keys = ['shopId', 'transmissionDate', 'transactionId',
'sequenceNb', 'ctxMode']
kw = dict(
transactionId=transactionId,
ctxMode=self.getPayzenVadsCtxMode(),
shopId=self.getServiceUsername(),
sequenceNb=1,
transmissionDate=transmissionDate,
)
kw['wsSignature'] = self._getSignature(kw, sorted_keys)
data = client.service.getInfo(**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)
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:
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_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)
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)
else:
try:
signature = self._check_transactionInfoSignature(data)
except Exception:
LOG('PayzenService', WARNING, 'Issue during signature calculation:',
error=True)
signature = False
del(os.environ['TZ'])
time.tzset()
return wrapped
try:
last_sent = str(client.last_sent())
except Exception:
LOG('PayzenService', WARNING,
'Issue during converting last_sent to string:', error=True)
signature = False
if present:
os.environ['TZ'] = tz
else:
del(os.environ['TZ'])
time.tzset()
try:
last_received = str(client.last_received())
except Exception:
LOG('PayzenService', WARNING,
'Issue during converting last_received to string:', error=True)
signature = False
class PayzenREST:
"""REST communication
Methods are returning list of:
* parsed response
* sent Data
* received Data
"""
def callPayzenApi(self, URL, payzen_dict):
base64string = base64.encodestring(
'%s:%s' % (
self.getServiceUsername(),
self.getServiceApiKey())).replace('\n', '')
header = {"Authorization": "Basic %s" % base64string}
LOG('callPayzenApi', WARNING,
"data = %s URL = %s" % (str(payzen_dict), URL), error=False)
# send data
result = requests.post(URL, data=payzen_dict, headers=header)
try:
data = result.json()
except Exception:
data = {}
LOG('PayzenService', WARNING,
'Issue during processing data_kw:', error=True)
return data, result.text
@setUTCTimeZone
def rest_getInfo(self, transmissionDate, transactionId):
"""Returns getInfo as dict, booelan, string, string
transmissionDate is "raw" date in format YYYYMMDD, without any marks
transactionId is id of transaction for this date
As soon as communication happeneded does not raise.
"""
URL = "https://api.payzen.eu/api-payment/V4/Order/Get"
kw = dict(
orderId=transactionId,
)
sent_data = str(kw)
data_kw, received_data = self.callPayzenApi(URL, kw)
return [data_kw, sent_data, received_data]
return [data_kw, signature, last_sent, last_received]
finally:
if present:
os.environ['TZ'] = tz
else:
del(os.environ['TZ'])
time.tzset()
from erp5.component.interface.IPaymentService import IPaymentService
class PayzenService(XMLObject, PayzenSOAP):
class PayzenService(XMLObject, PayzenREST):
meta_type = 'Payzen Service'
portal_type = 'Payzen Service'
......
......@@ -6,10 +6,22 @@
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>PayzenService</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>document.erp5.PayzenService</string> </value>
......@@ -24,6 +36,24 @@
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple>
<string>W: 24, 4: Redefining name \'tz\' from outer scope (line 15) (redefined-outer-name)</string>
<string>W: 23, 4: Redefining name \'present\' from outer scope (line 14) (redefined-outer-name)</string>
<string>W:190, 4: Unreachable code (unreachable)</string>
<string>W:195, 4: Unreachable code (unreachable)</string>
<string>W:200, 4: Unreachable code (unreachable)</string>
</tuple>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
......@@ -31,13 +61,28 @@
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
......@@ -50,7 +95,7 @@
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
......@@ -59,7 +104,7 @@
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
......@@ -74,33 +119,6 @@
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>ERP5TypeTestCase</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>time</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1377844605.75</float>
<string>GMT+9</string>
</tuple>
</state>
</object>
</value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/string</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>service_api_key_property</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
<item>
<key> <string>read_permission</string> </key>
<value> <string>Manage portal</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -106,6 +106,7 @@
<list>
<string>my_link_url_string</string>
<string>my_service_username</string>
<string>my_service_api_key</string>
<string>my_service_password</string>
<string>my_payzen_vads_ctx_mode</string>
<string>my_payzen_vads_version</string>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="StringField" module="Products.Formulator.StandardFields"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>my_service_api_key</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
<item>
<key> <string>required_not_found</string> </key>
<value> <string>Input is required but no input given.</string> </value>
</item>
<item>
<key> <string>too_long</string> </key>
<value> <string>Too much input was given.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>truncate</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>truncate</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>alternate_name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>css_class</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_maxwidth</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <int>20</int> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>external_validator</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Password</string> </value>
</item>
<item>
<key> <string>truncate</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>unicode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>whitespace_preserve</string> </key>
<value> <int>0</int> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -227,6 +227,10 @@
<key> <string>hidden</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
......@@ -237,7 +241,7 @@
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Certificate</string> </value>
<value> <string>Key</string> </value>
</item>
<item>
<key> <string>truncate</string> </key>
......
......@@ -227,6 +227,10 @@
<key> <string>hidden</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
......@@ -237,7 +241,7 @@
</item>
<item>
<key> <string>title</string> </key>
<value> <string>VADS Site ID</string> </value>
<value> <string>User</string> </value>
</item>
<item>
<key> <string>truncate</string> </key>
......
  • @rafael @tomo I think this erp5-vifib branch was rebased. Tests on 1.0 now fail with

    2021-01-21 23:21:14 slapos[16081] INFO fatal: Could not parse object 'ceaef5a3c5469a04f5650376cda54cb162abf38a'.

    wouldn't it be easier to use a dedicated private repository for the vifib fork ?

  • wouldn't it be easier to use a dedicated private repository for the vifib fork ?

    one day, we will merge our changes to master :)

    Anyway, as soon as we do rebase instead of merge, we can lose our commits at any time so it means we must tag all the commits that appear in slapos 1.0 branch to be sure to never lose them due to garbage collector.

  • Thanks, yes, hopefully these changes will be merged :)

    Note that it does not have to be a tag, it can also be a branch. Also branches can be a folder, so we can use vifib/something. Not sure it's better, it's just possible.

    FYI, this commit will not be garbage collected because there is discussion on it. What gitlab uses is another namespace, branches are in refs/heads/* and in git config we have by default:

    [remote "origin"]
      url = https://lab.nexedi.com/nexedi/erp5.git
      fetch = +refs/heads/*:refs/remotes/origin/*

    gitlab uses refs/keep-around to "keep around" (prevent garbage collection) of commits, so if you change git config to :

    [remote "origin"]
      url = https://lab.nexedi.com/nexedi/erp5.git
      fetch = +refs/heads/*:refs/remotes/origin/*
      fetch = +refs/keep-around/*:refs/remotes/keep-around/*

    and git fetch again, you can get this commit locally. Then you can push it again in a new branch and this repairs the failing build.

    There's this Could not parse object error, because the commit is not fetched by default, because git only fetches branches (refs/heads) and tags (refs/tags), it's still there somewhere in refs/keep-around, which is not fetched by default.

  • Oups, I didn't see your comment :( it would have been much easier for me...

    So I went to git clone --mirror in order to retrieve everything and then I tagged from there. I used erp5-vifib-20201229 tag name.

  • it was my mistake to not push a tag along with the rebase, sorry on this and thanks to @tomo for fix.

Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment