[IMP] payment, payment_*: add/improve tokenization support

This commit improves the tokenization process, allowing users
to set the acquirer to save payment data never, always or letting
the customer decide (only implemented in ecommerce for now).

Support for tokenization is improved for Ogone and Authorize.net

This commits also factorizes advanced payment features support
(authorization, tokenization, fees computation) in a generic method
overriden by every provider to specifiy which features are supported.
This process can be used in views to hide/show fields depending on
feature support or in constraint checks.
This commit is contained in:
Damien Bouvy
2016-09-01 15:58:41 +02:00
parent d9c9d0c96b
commit ffd527d027
7 changed files with 168 additions and 27 deletions
+38 -5
View File
@@ -101,8 +101,18 @@ class PaymentAcquirer(models.Model):
'Error Message', translate=True,
default='<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order.',
help='Message displayed, if error is occur during the payment process.')
save_token = fields.Selection([
('none', 'Never'),
('ask', 'Let the customer decide'),
('always', 'Always')],
string='Store Card Data', default='none',
help="Determine if card data is saved as a token automatically or not. "
"Payment tokens allow your customer to reuse their cards in the e-commerce "
"or allow you to charge an invoice directly on a credit card. If set to "
"'let the customer decide', ecommerce customers will have a checkbox displayed on the payment page.")
token_implemented = fields.Boolean('Saving Card Data supported', compute='_compute_feature_support')
fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_fees_implemented')
fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_feature_support')
fees_active = fields.Boolean('Add Extra Fees')
fees_dom_fixed = fields.Float('Fixed domestic fees')
fees_dom_var = fields.Float('Variable domestic fees (in percents)')
@@ -127,9 +137,12 @@ class PaymentAcquirer(models.Model):
"resized as a 64x64px image, with aspect ratio preserved. "
"Use this field anywhere a small image is required.")
@api.one
def _compute_fees_implemented(self):
self.fees_implemented = hasattr(self, '%s_compute_fees' % self.provider)
@api.multi
def _compute_feature_support(self):
feature_support = self._get_feature_support()
for acquirer in self:
acquirer.fees_implemented = acquirer.provider in feature_support['fees']
acquirer.token_implemented = acquirer.provider in feature_support['tokenize']
@api.multi
def _check_required_if_provider(self):
@@ -140,10 +153,30 @@ class PaymentAcquirer(models.Model):
return False
return True
@api.constrains('auto_confirm')
def _check_authorization_support(self):
for acquirer in self:
if acquirer.auto_confirm == 'authorize' and acquirer.provider not in self._get_feature_support()['authorize']:
raise ValidationError('Transaction Authorization is not supported by this payment provider.')
return True
_constraints = [
(_check_required_if_provider, 'Required fields not filled', []),
]
def _get_feature_support(self):
"""Get advanced feature support by provider.
Each provider should add its technical in the corresponding
key for the following features:
* fees: support payment fees computations
* authorize: support authorizing payment (separates
authorization and capture)
* tokenize: support saving payment data in a payment.tokenize
object
"""
return dict(authorize=[], tokenize=[], fees=[])
@api.model
def create(self, vals):
image_resize_images(vals)
@@ -370,7 +403,7 @@ class PaymentTransaction(models.Model):
type = fields.Selection([
('server2server', 'Server To Server'),
('form', 'Form'),
('form_save', 'Form with credentials storage')], 'Type',
('form_save', 'Form with tokenization')], 'Type',
default='form', required=True)
state = fields.Selection([
('draft', 'Draft'),
+2
View File
@@ -25,6 +25,7 @@
<form string="Payment Acquirer">
<field name="environment" invisible='1'/>
<field name="fees_implemented" invisible='1'/>
<field name="token_implemented" invisible='1'/>
<sheet>
<field name="module_id" invisible="1"/>
<field name="module_state" invisible="1"/>
@@ -79,6 +80,7 @@
<page string="Configuration">
<group name="acquirer_config">
<field name="auto_confirm" widget="radio"/>
<field name="save_token" widget="radio" attrs="{'invisible': [('token_implemented', '=', False)]}"/>
<field name="journal_id" attrs="{'required': [('auto_confirm', '=', 'generate_and_pay_invoice')], 'invisible': [('auto_confirm', '!=', 'generate_and_pay_invoice')]}"/>
<label for="view_template_id" groups="base.group_no_one"/>
<div groups="base.group_no_one">
@@ -126,6 +126,42 @@ class AuthorizeAPI():
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
return res
def create_customer_profile_from_tx(self, partner, transaction_id):
"""Create an Auth.net payment/customer profile from an existing transaction.
Creates a customer profile for the partner/credit card combination and links
a corresponding payment profile to it. Note that a single partner in the Odoo
database can have multiple customer profiles in Authorize.net (i.e. a customer
profile is created for every res.partner/payment.token couple).
Note that this function makes 2 calls to the authorize api, since we need to
obtain a partial cardnumber to generate a meaningful payment.token name.
:param record partner: the res.partner record of the customer
:param str transaction_id: id of the authorized transaction in the
Authorize.net backend
:return: a dict containing the profile_id and payment_profile_id of the
newly created customer profile and payment profile as well as the
last digits of the card number
:rtype: dict
"""
root = self._base_tree('createCustomerProfileFromTransactionRequest')
etree.SubElement(root, "transId").text = transaction_id
customer = etree.SubElement(root, "customer")
etree.SubElement(customer, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8])
etree.SubElement(customer, "email").text = partner.email
response = self._authorize_request(root)
res = dict()
res['profile_id'] = response.find('customerProfileId').text
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
root_profile = self._base_tree('getCustomerPaymentProfileRequest')
etree.SubElement(root_profile, "customerProfileId").text = res['profile_id']
etree.SubElement(root_profile, "customerPaymentProfileId").text = res['payment_profile_id']
response_profile = self._authorize_request(root_profile)
res['name'] = response_profile.find('paymentProfile/payment/creditCard/cardNumber').text
return res
# Transaction management
def auth_and_capture(self, token, amount, reference):
"""Authorize and capture a payment for the given amount.
@@ -193,7 +229,7 @@ class AuthorizeAPI():
Capture a previsouly authorized payment. Note that the amount is required
even though we do not support partial capture.
:param str transaction_id: the id of the authorized transaction in the
:param str transaction_id: id of the authorized transaction in the
Authorize.net backend
:param str amount: transaction amount (up to 15 digits with decimal point)
+41 -5
View File
@@ -24,6 +24,22 @@ class PaymentAcquirerAuthorize(models.Model):
authorize_login = fields.Char(string='API Login Id', required_if_provider='authorize')
authorize_transaction_key = fields.Char(string='API Transaction Key', required_if_provider='authorize')
def _get_feature_support(self):
"""Get advanced feature support by provider.
Each provider should add its technical in the corresponding
key for the following features:
* fees: support payment fees computations
* authorize: support authorizing payment (separates
authorization and capture)
* tokenize: support saving payment data in a payment.tokenize
object
"""
res = super(PaymentAcquirerAuthorize, self)._get_feature_support()
res['authorize'].append('authorize')
res['tokenize'].append('authorize')
return res
def _get_authorize_urls(self, environment):
""" Authorize URLs """
if environment == 'prod':
@@ -169,11 +185,29 @@ class TxAuthorize(models.Model):
return True
status_code = int(data.get('x_response_code', '0'))
if status_code == self._authorize_valid_tx_status:
self.write({
'state': 'done' if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture'] else 'authorized',
'acquirer_reference': data.get('x_trans_id'),
'date_validate': fields.Datetime.now() if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture'] else False,
})
if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture']:
self.write({
'state': 'done',
'acquirer_reference': data.get('x_trans_id'),
'date_validate': fields.Datetime.now(),
})
elif data.get('x_type').lower() in ['auth_only']:
self.write({
'state': 'authorized',
'acquirer_reference': data.get('x_trans_id'),
})
if self.partner_id and not self.payment_token_id and \
(self.type == 'form_save' or self.acquirer_id.save_token == 'always'):
transaction = AuthorizeAPI(self.acquirer_id)
res = transaction.create_customer_profile_from_tx(self.partner_id, self.acquirer_reference)
token_id = self.env['payment.token'].create({
'authorize_profile': res.get('profile_id'),
'name': res.get('name'),
'acquirer_ref': res.get('payment_profile_id'),
'acquirer_id': self.acquirer_id.id,
'partner_id': self.partner_id.id,
})
self.payment_token_id = token_id
return True
elif status_code == self._authorize_pending_tx_status:
self.write({
@@ -299,3 +333,5 @@ class PaymentToken(models.Model):
}
else:
raise ValidationError('The Customer Profile creation in Authorize.NET failed.')
else:
return values
+34 -15
View File
@@ -32,10 +32,25 @@ class PaymentAcquirerOgone(models.Model):
ogone_password = fields.Char('API User Password', required_if_provider='ogone')
ogone_shakey_in = fields.Char('SHA Key IN', size=32, required_if_provider='ogone')
ogone_shakey_out = fields.Char('SHA Key OUT', size=32, required_if_provider='ogone')
ogone_alias_usage = fields.Char('Alias Usage', help="""If you want to use Ogone Aliases,
this default Alias Usage will be presented to
the customer as the reason you want to
keep his payment data""")
ogone_alias_usage = fields.Char('Alias Usage', default="Allow saving my payment data",
help="If you want to use Ogone Aliases, this default "
"Alias Usage will be presented to the customer as the "
"reason you want to keep his payment data")
def _get_feature_support(self):
"""Get advanced feature support by provider.
Each provider should add its technical in the corresponding
key for the following features:
* fees: support payment fees computations
* authorize: support authorizing payment (separates
authorization and capture)
* tokenize: support saving payment data in a payment.tokenize
object
"""
res = super(PaymentAcquirerOgone, self)._get_feature_support()
res['tokenize'].append('ogone')
return res
def _get_ogone_urls(self, environment):
""" Ogone URLS:
@@ -156,7 +171,7 @@ class PaymentAcquirerOgone(models.Model):
'CANCELURL': '%s' % urlparse.urljoin(base_url, OgoneController._cancel_url),
'PARAMPLUS': 'return_url=%s' % ogone_tx_values.pop('return_url') if ogone_tx_values.get('return_url') else False,
}
if values.get('type') == 'form_save':
if self.save_token in ['ask', 'always']:
temp_ogone_tx_values.update({
'ALIAS': 'ODOO-NEW-ALIAS-%s' % time.time(), # something unique,
'ALIASUSAGE': values.get('alias_usage') or self.ogone_alias_usage,
@@ -238,17 +253,17 @@ class PaymentTxOgone(models.Model):
tx.acquirer_reference = pay_id
# alias was created on ogone server, store it
if alias:
Method = self.env['payment.token']
if alias and tx.type == 'form_save':
Token = self.env['payment.token']
domain = [('acquirer_ref', '=', alias)]
cardholder = data.get('CN')
if not Method.search_count(domain):
if not Token.search_count(domain):
_logger.info('Ogone: saving alias %s for partner %s' % (data.get('CARDNO'), tx.partner_id))
ref = Method.create({'name': data.get('CARDNO') + (' - ' + cardholder if cardholder else ''),
'partner_id': tx.partner_id.id,
'acquirer_id': tx.acquirer_id.id,
'acquirer_ref': alias})
tx.write({'payment_token_id': ref})
ref = Token.create({'name': data.get('CARDNO') + (' - ' + cardholder if cardholder else ''),
'partner_id': tx.partner_id.id,
'acquirer_id': tx.acquirer_id.id,
'acquirer_ref': alias})
tx.write({'payment_token_id': ref.id})
return tx
@@ -278,7 +293,9 @@ class PaymentTxOgone(models.Model):
'date_validate': datetime.datetime.strptime(data['TRXDATE'], '%m/%d/%y').strftime(DEFAULT_SERVER_DATE_FORMAT),
'acquirer_reference': data['PAYID'],
}
if data.get('ALIAS') and self.partner_id and self.type == 'form_save' and not self.payment_token_id:
if data.get('ALIAS') and self.partner_id and \
(self.type == 'form_save' or self.acquirer_id.save_token == 'always')\
and not self.payment_token_id:
pm = self.env['payment.token'].create({
'partner_id': self.partner_id.id,
'acquirer_id': self.acquirer_id.id,
@@ -383,7 +400,9 @@ class PaymentTxOgone(models.Model):
'date_validate': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT),
'acquirer_reference': tree.get('PAYID'),
})
if tree.get('ALIAS') and self.partner_id and self.type == 'form_save' and not self.payment_token_id:
if tree.get('ALIAS') and self.partner_id and \
(self.type == 'form_save' or self.acquirer_id.save_token == 'always')\
and not self.payment_token_id:
pm = self.env['payment.token'].create({
'partner_id': self.partner_id.id,
'acquirer_id': self.acquirer_id.id,
@@ -19,7 +19,7 @@
<input type='hidden' name='OWNERCTY' t-att-value='OWNERCTY'/>
<input type='hidden' name='OWNERTOWN' t-att-value='OWNERTOWN'/>
<input type='hidden' name='OWNERTELNO' t-att-value='OWNERTELNO'/>
<t t-if="type == 'form_save'">
<t t-if="acquirer.save_token in ['ask', 'always']">
<input type='hidden' name='ALIAS' t-att-value='ALIAS'/>
<input type='hidden' name='ALIASUSAGE' t-att-value='ALIASUSAGE'/>
</t>
+15
View File
@@ -34,6 +34,21 @@ class AcquirerPaypal(models.Model):
fees_int_fixed = fields.Float(default=0.35)
fees_int_var = fields.Float(default=3.9)
def _get_feature_support(self):
"""Get advanced feature support by provider.
Each provider should add its technical in the corresponding
key for the following features:
* fees: support payment fees computations
* authorize: support authorizing payment (separates
authorization and capture)
* tokenize: support saving payment data in a payment.tokenize
object
"""
res = super(AcquirerPaypal, self)._get_feature_support()
res['fees'].append('paypal')
return res
@api.model
def _get_paypal_urls(self, environment):
""" Paypal URLS """