2012-07-26 10:22:22 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
import sha
|
|
|
|
from appy import Object
|
|
|
|
from appy.gen import Type
|
|
|
|
from appy.shared.utils import normalizeString
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class OgoneConfig:
|
|
|
|
'''If you plan, in your app, to perform on-line payments via the Ogone (r)
|
|
|
|
system, create an instance of this class in your app and place it in the
|
|
|
|
'ogone' attr of your appy.gen.Config instance.'''
|
|
|
|
def __init__(self):
|
|
|
|
# self.env refers to the Ogone environment and can be "test" or "prod".
|
|
|
|
self.env = 'test'
|
|
|
|
# You merchant Ogone ID
|
|
|
|
self.PSPID = None
|
|
|
|
# Default currency for transactions
|
|
|
|
self.currency = 'EUR'
|
|
|
|
# Default language
|
|
|
|
self.language = 'en_US'
|
|
|
|
# SHA-IN key (digest will be generated with the SHA-1 algorithm)
|
|
|
|
self.shaInKey = ''
|
|
|
|
# SHA-OUT key (digest will be generated with the SHA-1 algorithm)
|
|
|
|
self.shaOutKey = ''
|
|
|
|
|
|
|
|
def __repr__(self): return str(self.__dict__)
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class Ogone(Type):
|
|
|
|
'''This field allows to perform payments with the Ogone (r) system.'''
|
|
|
|
urlTypes = ('accept', 'decline', 'exception', 'cancel')
|
|
|
|
|
|
|
|
def __init__(self, orderMethod, responseMethod, show='view', page='main',
|
|
|
|
group=None, layouts=None, move=0, specificReadPermission=False,
|
|
|
|
specificWritePermission=False, width=None, height=None,
|
|
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
|
|
mapping=None, label=None):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, None, (0,1), None, show, page, group, layouts, move,
|
|
|
|
False, False,specificReadPermission,
|
2012-07-26 10:22:22 -05:00
|
|
|
specificWritePermission, width, height, None, colspan,
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
master, masterValue, focus, False, True, mapping, label,
|
|
|
|
None)
|
2012-07-26 10:22:22 -05:00
|
|
|
# orderMethod must contain a method returning a dict containing info
|
|
|
|
# about the order. Following keys are mandatory:
|
2012-08-10 09:00:41 -05:00
|
|
|
# * orderID An identifier for the order. Don't use the object UID
|
|
|
|
# for this, use a random number, because if the payment
|
|
|
|
# is canceled, Ogone will not allow you to reuse the same
|
|
|
|
# orderID for the next tentative.
|
2012-07-26 10:22:22 -05:00
|
|
|
# * amount An integer representing the price for this order,
|
|
|
|
# multiplied by 100 (no floating point value, no commas
|
|
|
|
# are tolerated. Dont't forget to multiply the amount by
|
2012-08-07 10:38:54 -05:00
|
|
|
# 100!
|
2012-07-26 10:22:22 -05:00
|
|
|
self.orderMethod = orderMethod
|
|
|
|
# responseMethod must contain a method accepting one param, let's call
|
|
|
|
# it "response". The response method will be called when we will get
|
|
|
|
# Ogone's response about the status of the payment. Param "response" is
|
|
|
|
# an object whose attributes correspond to all parameters that you have
|
2012-08-07 10:38:54 -05:00
|
|
|
# chosen to receive in your Ogone merchant account. After the payment,
|
|
|
|
# the user will be redirected to the object's view page, excepted if
|
|
|
|
# your method returns an alternatve URL.
|
2012-07-26 10:22:22 -05:00
|
|
|
self.responseMethod = responseMethod
|
|
|
|
|
|
|
|
noShaInKeys = ('env',)
|
|
|
|
noShaOutKeys = ('name', 'SHASIGN')
|
|
|
|
def createShaDigest(self, values, passphrase, keysToIgnore=()):
|
|
|
|
'''Creates an Ogone-compliant SHA-1 digest based on key-value pairs in
|
|
|
|
dict p_values and on some p_passphrase.'''
|
|
|
|
# Create a new dict by removing p_keysToIgnore from p_values, and by
|
|
|
|
# upperizing all keys.
|
|
|
|
shaRes = {}
|
|
|
|
for k, v in values.iteritems():
|
|
|
|
if k in keysToIgnore: continue
|
2012-08-10 09:00:41 -05:00
|
|
|
# Ogone: we must not include empty values.
|
|
|
|
if (v == None) or (v == ''): continue
|
2012-07-26 10:22:22 -05:00
|
|
|
shaRes[k.upper()] = v
|
|
|
|
# Create a sorted list of keys
|
|
|
|
keys = shaRes.keys()
|
|
|
|
keys.sort()
|
|
|
|
shaList = []
|
|
|
|
for k in keys:
|
|
|
|
shaList.append('%s=%s' % (k, shaRes[k]))
|
|
|
|
shaObject = sha.new(passphrase.join(shaList) + passphrase)
|
|
|
|
res = shaObject.hexdigest()
|
|
|
|
return res
|
|
|
|
|
|
|
|
def getValue(self, obj):
|
|
|
|
'''The "value" of the Ogone field is a dict that collects all the
|
|
|
|
necessary info for making the payment.'''
|
|
|
|
tool = obj.getTool()
|
|
|
|
# Basic Ogone parameters were generated in the app config module.
|
|
|
|
res = obj.getProductConfig().ogone.copy()
|
|
|
|
shaKey = res['shaInKey']
|
2012-08-10 09:00:41 -05:00
|
|
|
# Remove elements from the Ogone config that we must not send in the
|
2012-07-26 10:22:22 -05:00
|
|
|
# payment request.
|
|
|
|
del res['shaInKey']
|
|
|
|
del res['shaOutKey']
|
|
|
|
res.update(self.callMethod(obj, self.orderMethod))
|
|
|
|
# Add user-related information
|
|
|
|
res['CN'] = str(normalizeString(tool.getUserName()))
|
|
|
|
user = obj.appy().appyUser
|
|
|
|
res['EMAIL'] = user.email or user.login
|
|
|
|
# Add standard back URLs
|
|
|
|
siteUrl = tool.getSiteUrl()
|
|
|
|
res['catalogurl'] = siteUrl
|
|
|
|
res['homeurl'] = siteUrl
|
|
|
|
# Add redirect URLs
|
|
|
|
for t in self.urlTypes:
|
|
|
|
res['%surl' % t] = '%s/onProcess' % obj.absolute_url()
|
|
|
|
# Add additional parameter that we want Ogone to give use back in all
|
|
|
|
# of its responses: the name of this Appy Ogone field. This way, Appy
|
|
|
|
# will be able to call method m_process below, that will process
|
|
|
|
# Ogone's response.
|
|
|
|
res['paramplus'] = 'name=%s' % self.name
|
|
|
|
# Ensure every value is a str
|
|
|
|
for k in res.iterkeys():
|
|
|
|
if not isinstance(res[k], str):
|
|
|
|
res[k] = str(res[k])
|
|
|
|
# Compute a SHA-1 key as required by Ogone and add it to the res
|
|
|
|
res['SHASign'] = self.createShaDigest(res, shaKey,
|
|
|
|
keysToIgnore=self.noShaInKeys)
|
|
|
|
return res
|
|
|
|
|
|
|
|
def ogoneResponseOk(self, obj):
|
|
|
|
'''Returns True if the SHA-1 signature from Ogone matches retrieved
|
|
|
|
params.'''
|
|
|
|
response = obj.REQUEST.form
|
|
|
|
shaKey = obj.getProductConfig().ogone['shaOutKey']
|
|
|
|
digest = self.createShaDigest(response, shaKey,
|
|
|
|
keysToIgnore=self.noShaOutKeys)
|
2012-08-07 10:38:54 -05:00
|
|
|
return digest.lower() == response['SHASIGN'].lower()
|
2012-07-26 10:22:22 -05:00
|
|
|
|
|
|
|
def process(self, obj):
|
|
|
|
'''Processes a response from Ogone.'''
|
|
|
|
# Call the response method defined in this Ogone field.
|
|
|
|
if not self.ogoneResponseOk(obj):
|
2012-08-07 10:38:54 -05:00
|
|
|
obj.log('Ogone response SHA failed. REQUEST: %s' % \
|
2012-07-26 10:22:22 -05:00
|
|
|
str(obj.REQUEST.form))
|
|
|
|
raise Exception('Failure, possible fraud detection, an ' \
|
|
|
|
'administrator has been contacted.')
|
|
|
|
# Create a nice object from the form.
|
|
|
|
response = Object()
|
2012-08-07 10:38:54 -05:00
|
|
|
for k, v in obj.REQUEST.form.iteritems():
|
2012-07-26 10:22:22 -05:00
|
|
|
setattr(response, k, v)
|
2012-08-07 10:38:54 -05:00
|
|
|
# Call the field method that handles the response received from Ogone.
|
|
|
|
url = self.responseMethod(obj.appy(), response)
|
|
|
|
# Redirect the user to the correct page. If the field method returns
|
|
|
|
# some URL, use it. Else, use the view page of p_obj.
|
|
|
|
if not url: url = obj.absolute_url()
|
|
|
|
obj.goto(url)
|
2012-07-26 10:22:22 -05:00
|
|
|
# ------------------------------------------------------------------------------
|