[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:
@@ -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'),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 """
|
||||
|
||||
Reference in New Issue
Block a user