Commit f7aec705 authored by Cédric Le Ninivin's avatar Cédric Le Ninivin Committed by Rafael Monnerat

erp5_payzen_secure_payment: Move Payzen to REST API

* Soap Duplicate and Soap Cancel functions have been discontinued due to lack of usage
* OrderId is now necessary in order to retrieve Transactions
parent 7e2c6a2b
...@@ -4,9 +4,11 @@ from Products.ERP5Type import Permissions, PropertySheet ...@@ -4,9 +4,11 @@ from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type.XMLObject import XMLObject
import hashlib import hashlib
from zLOG import LOG, WARNING from zLOG import LOG, WARNING
import base64
import datetime import datetime
import os import os
import time import time
import requests
from Products.DCWorkflow.DCWorkflow import ValidationFailed from Products.DCWorkflow.DCWorkflow import ValidationFailed
present = False present = False
...@@ -16,250 +18,79 @@ if 'TZ' in os.environ: ...@@ -16,250 +18,79 @@ if 'TZ' in os.environ:
tz = os.environ['TZ'] tz = os.environ['TZ']
os.environ['TZ'] = 'UTC' os.environ['TZ'] = 'UTC'
time.tzset() time.tzset()
try: def setUTCTimeZone(fn):
import suds def wrapped(*args, **kwargs):
except ImportError: present = False
class PayzenSOAP: tz = None
pass if 'TZ' in os.environ:
else: present = True
def setUTCTimeZone(fn): tz = os.environ['TZ']
def wrapped(*args, **kwargs): os.environ['TZ'] = 'UTC'
present = False time.tzset()
tz = None try:
if 'TZ' in os.environ: return fn(*args, **kwargs)
present = True finally:
tz = os.environ['TZ'] if present:
os.environ['TZ'] = 'UTC' os.environ['TZ'] = tz
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)
else: else:
try: del(os.environ['TZ'])
signature = self._check_transactionInfoSignature(data) time.tzset()
except Exception: return wrapped
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
try: if present:
last_sent = str(client.last_sent()) os.environ['TZ'] = tz
except Exception: else:
LOG('PayzenService', WARNING, del(os.environ['TZ'])
'Issue during converting last_sent to string:', error=True) time.tzset()
signature = False
try: class PayzenREST:
last_received = str(client.last_received()) """REST communication
except Exception:
LOG('PayzenService', WARNING, Methods are returning list of:
'Issue during converting last_received to string:', error=True) * parsed response
signature = False * 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 from erp5.component.interface.IPaymentService import IPaymentService
class PayzenService(XMLObject, PayzenSOAP): class PayzenService(XMLObject, PayzenREST):
meta_type = 'Payzen Service' meta_type = 'Payzen Service'
portal_type = 'Payzen Service' portal_type = 'Payzen Service'
......
...@@ -6,10 +6,22 @@ ...@@ -6,10 +6,22 @@
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item> <item>
<key> <string>default_reference</string> </key> <key> <string>default_reference</string> </key>
<value> <string>PayzenService</string> </value> <value> <string>PayzenService</string> </value>
</item> </item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>document.erp5.PayzenService</string> </value> <value> <string>document.erp5.PayzenService</string> </value>
...@@ -24,6 +36,24 @@ ...@@ -24,6 +36,24 @@
<none/> <none/>
</value> </value>
</item> </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> <item>
<key> <string>version</string> </key> <key> <string>version</string> </key>
<value> <string>erp5</string> </value> <value> <string>erp5</string> </value>
...@@ -31,13 +61,28 @@ ...@@ -31,13 +61,28 @@
<item> <item>
<key> <string>workflow_history</string> </key> <key> <string>workflow_history</string> </key>
<value> <value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
</record> </record>
<record id="2" aka="AAAAAAAAAAI="> <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> <pickle>
<global name="PersistentMapping" module="Persistence.mapping"/> <global name="PersistentMapping" module="Persistence.mapping"/>
</pickle> </pickle>
...@@ -50,7 +95,7 @@ ...@@ -50,7 +95,7 @@
<item> <item>
<key> <string>component_validation_workflow</string> </key> <key> <string>component_validation_workflow</string> </key>
<value> <value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> <persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value> </value>
</item> </item>
</dictionary> </dictionary>
...@@ -59,7 +104,7 @@ ...@@ -59,7 +104,7 @@
</dictionary> </dictionary>
</pickle> </pickle>
</record> </record>
<record id="3" aka="AAAAAAAAAAM="> <record id="4" aka="AAAAAAAAAAQ=">
<pickle> <pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/> <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle> </pickle>
...@@ -74,33 +119,6 @@ ...@@ -74,33 +119,6 @@
<key> <string>action</string> </key> <key> <string>action</string> </key>
<value> <string>validate</string> </value> <value> <string>validate</string> </value>
</item> </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> <item>
<key> <string>validation_state</string> </key> <key> <string>validation_state</string> </key>
<value> <string>validated</string> </value> <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 @@ ...@@ -106,6 +106,7 @@
<list> <list>
<string>my_link_url_string</string> <string>my_link_url_string</string>
<string>my_service_username</string> <string>my_service_username</string>
<string>my_service_api_key</string>
<string>my_service_password</string> <string>my_service_password</string>
<string>my_payzen_vads_ctx_mode</string> <string>my_payzen_vads_ctx_mode</string>
<string>my_payzen_vads_version</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 @@ ...@@ -227,6 +227,10 @@
<key> <string>hidden</string> </key> <key> <string>hidden</string> </key>
<value> <int>0</int> </value> <value> <int>0</int> </value>
</item> </item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item> <item>
<key> <string>max_length</string> </key> <key> <string>max_length</string> </key>
<value> <string></string> </value> <value> <string></string> </value>
...@@ -237,7 +241,7 @@ ...@@ -237,7 +241,7 @@
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
<value> <string>Certificate</string> </value> <value> <string>Key</string> </value>
</item> </item>
<item> <item>
<key> <string>truncate</string> </key> <key> <string>truncate</string> </key>
......
...@@ -227,6 +227,10 @@ ...@@ -227,6 +227,10 @@
<key> <string>hidden</string> </key> <key> <string>hidden</string> </key>
<value> <int>0</int> </value> <value> <int>0</int> </value>
</item> </item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item> <item>
<key> <string>max_length</string> </key> <key> <string>max_length</string> </key>
<value> <string></string> </value> <value> <string></string> </value>
...@@ -237,7 +241,7 @@ ...@@ -237,7 +241,7 @@
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
<value> <string>VADS Site ID</string> </value> <value> <string>User</string> </value>
</item> </item>
<item> <item>
<key> <string>truncate</string> </key> <key> <string>truncate</string> </key>
......
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