summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDamien Bouvy <dbo@odoo.com>2016-09-01 13:07:12 +0200
committerDamien Bouvy <dbo@odoo.com>2016-09-01 15:58:41 +0200
commitffd527d0273197925102175feb95d4a6f4fcd8d5 (patch)
tree80281badff239c5d4a6e0a4cc23bb75eb558d4fd
parentd9c9d0c96b2aaf66da476adae8f8e935bd7dfebf (diff)
[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.
-rw-r--r--addons/payment/models/payment_acquirer.py45
-rw-r--r--addons/payment/views/payment_views.xml2
-rw-r--r--addons/payment_authorize/models/authorize_request.py38
-rw-r--r--addons/payment_authorize/models/payment.py46
-rw-r--r--addons/payment_ogone/models/payment.py49
-rw-r--r--addons/payment_ogone/views/payment_ogone_templates.xml2
-rw-r--r--addons/payment_paypal/models/payment.py15
7 files changed, 169 insertions, 28 deletions
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index d0733c16fe3..d9442e0ae1c 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -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.')
-
- fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_fees_implemented')
+ 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_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'),
diff --git a/addons/payment/views/payment_views.xml b/addons/payment/views/payment_views.xml
index 51bf831a719..a8677057aff 100644
--- a/addons/payment/views/payment_views.xml
+++ b/addons/payment/views/payment_views.xml
@@ -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">
diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py
index e94c4e1c484..28cba8db301 100644
--- a/addons/payment_authorize/models/authorize_request.py
+++ b/addons/payment_authorize/models/authorize_request.py
@@ -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)
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index af57569eb5f..db112af777d 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -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
diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py
index ef4ff0871a2..2282c5fede3 100644
--- a/addons/payment_ogone/models/payment.py
+++ b/addons/payment_ogone/models/payment.py
@@ -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,
diff --git a/addons/payment_ogone/views/payment_ogone_templates.xml b/addons/payment_ogone/views/payment_ogone_templates.xml
index 8e9dd72c103..63a5f4fd950 100644
--- a/addons/payment_ogone/views/payment_ogone_templates.xml
+++ b/addons/payment_ogone/views/payment_ogone_templates.xml
@@ -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>
diff --git a/addons/payment_paypal/models/payment.py b/addons/payment_paypal/models/payment.py
index 1248d36104b..624331a7647 100644
--- a/addons/payment_paypal/models/payment.py
+++ b/addons/payment_paypal/models/payment.py
@@ -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 """