mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 16:52:07 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -1,10 +1,11 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import urllib.parse
|
||||
import werkzeug
|
||||
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
|
|
@ -38,7 +39,7 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
)
|
||||
def payment_pay(
|
||||
self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None,
|
||||
provider_id=None, access_token=None, **kwargs
|
||||
access_token=None, **kwargs
|
||||
):
|
||||
""" Display the payment form with optional filtering of payment options.
|
||||
|
||||
|
|
@ -47,33 +48,34 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
|
||||
In addition to the desired filtering, a second one ensures that none of the following
|
||||
rules is broken:
|
||||
- Public users are not allowed to save their payment method as a token.
|
||||
- Payments made by public users should either *not* be made on behalf of a specific
|
||||
partner or have an access token validating the partner, amount and currency.
|
||||
We let access rights and security rules do their job for logged in users.
|
||||
|
||||
:param str reference: The custom prefix to compute the full reference
|
||||
:param str amount: The amount to pay
|
||||
:param str currency_id: The desired currency, as a `res.currency` id
|
||||
:param str partner_id: The partner making the payment, as a `res.partner` id
|
||||
:param str company_id: The related company, as a `res.company` id
|
||||
:param str provider_id: The desired provider, as a `payment.provider` id
|
||||
:param str access_token: The access token used to authenticate the partner
|
||||
- Public users are not allowed to save their payment method as a token.
|
||||
- Payments made by public users should either *not* be made on behalf of a specific partner
|
||||
or have an access token validating the partner, amount and currency.
|
||||
|
||||
We let access rights and security rules do their job for logged users.
|
||||
|
||||
:param str reference: The custom prefix to compute the full reference.
|
||||
:param str amount: The amount to pay.
|
||||
:param str currency_id: The desired currency, as a `res.currency` id.
|
||||
:param str partner_id: The partner making the payment, as a `res.partner` id.
|
||||
:param str company_id: The related company, as a `res.company` id.
|
||||
:param str access_token: The access token used to authenticate the partner.
|
||||
:param dict kwargs: Optional data passed to helper methods.
|
||||
:return: The rendered checkout form
|
||||
:return: The rendered payment form.
|
||||
:rtype: str
|
||||
:raise: werkzeug.exceptions.NotFound if the access token is invalid
|
||||
:raise NotFound: If the access token is invalid.
|
||||
"""
|
||||
# Cast numeric parameters as int or float and void them if their str value is malformed
|
||||
currency_id, provider_id, partner_id, company_id = tuple(map(
|
||||
self._cast_as_int, (currency_id, provider_id, partner_id, company_id)
|
||||
currency_id, partner_id, company_id = tuple(map(
|
||||
self._cast_as_int, (currency_id, partner_id, company_id)
|
||||
))
|
||||
amount = self._cast_as_float(amount)
|
||||
|
||||
# Raise an HTTP 404 if a partner is provided with an invalid access token
|
||||
if partner_id:
|
||||
if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
|
||||
raise werkzeug.exceptions.NotFound() # Don't leak information about ids.
|
||||
raise NotFound() # Don't leak information about ids.
|
||||
|
||||
user_sudo = request.env.user
|
||||
logged_in = not user_sudo._is_public()
|
||||
|
|
@ -104,60 +106,76 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
# Make sure that the currency exists and is active
|
||||
currency = request.env['res.currency'].browse(currency_id).exists()
|
||||
if not currency or not currency.active:
|
||||
raise werkzeug.exceptions.NotFound() # The currency must exist and be active.
|
||||
raise NotFound() # The currency must exist and be active.
|
||||
|
||||
# Select all providers and tokens that match the constraints
|
||||
availability_report = {}
|
||||
# Select all the payment methods and tokens that match the payment context.
|
||||
providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
|
||||
company_id, partner_sudo.id, amount, currency_id=currency.id, **kwargs
|
||||
) # In sudo mode to read the fields of providers and partner (if not logged in)
|
||||
if provider_id in providers_sudo.ids: # Only keep the desired provider if it's suitable
|
||||
providers_sudo = providers_sudo.browse(provider_id)
|
||||
payment_tokens = request.env['payment.token'].search(
|
||||
[('provider_id', 'in', providers_sudo.ids), ('partner_id', '=', partner_sudo.id)]
|
||||
) if logged_in else request.env['payment.token']
|
||||
company_id,
|
||||
partner_sudo.id,
|
||||
amount,
|
||||
currency_id=currency.id,
|
||||
report=availability_report,
|
||||
**kwargs,
|
||||
) # In sudo mode to read the fields of providers and partner (if logged out).
|
||||
payment_methods_sudo = request.env['payment.method'].sudo()._get_compatible_payment_methods(
|
||||
providers_sudo.ids,
|
||||
partner_sudo.id,
|
||||
currency_id=currency.id,
|
||||
report=availability_report,
|
||||
**kwargs,
|
||||
) # In sudo mode to read the fields of providers.
|
||||
tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
|
||||
providers_sudo.ids, partner_sudo.id
|
||||
) # In sudo mode to be able to read tokens of other partners and the fields of providers.
|
||||
|
||||
# Make sure that the partner's company matches the company passed as parameter.
|
||||
if not PaymentPortal._can_partner_pay_in_company(partner_sudo, company):
|
||||
providers_sudo = request.env['payment.provider'].sudo()
|
||||
payment_tokens = request.env['payment.token']
|
||||
|
||||
# Compute the fees taken by providers supporting the feature
|
||||
fees_by_provider = {
|
||||
provider_sudo: provider_sudo._compute_fees(amount, currency, partner_sudo.country_id)
|
||||
for provider_sudo in providers_sudo.filtered('fees_active')
|
||||
}
|
||||
company_mismatch = not PaymentPortal._can_partner_pay_in_company(partner_sudo, company)
|
||||
|
||||
# Generate a new access token in case the partner id or the currency id was updated
|
||||
access_token = payment_utils.generate_access_token(partner_sudo.id, amount, currency.id)
|
||||
|
||||
rendering_context = {
|
||||
'providers': providers_sudo,
|
||||
'tokens': payment_tokens,
|
||||
'fees_by_provider': fees_by_provider,
|
||||
'show_tokenize_input': self._compute_show_tokenize_input_mapping(
|
||||
providers_sudo, logged_in=logged_in, **kwargs
|
||||
portal_page_values = {
|
||||
'res_company': company, # Display the correct logo in a multi-company environment.
|
||||
'company_mismatch': company_mismatch,
|
||||
'expected_company': company,
|
||||
'partner_is_different': partner_is_different,
|
||||
}
|
||||
payment_form_values = {
|
||||
'show_tokenize_input_mapping': self._compute_show_tokenize_input_mapping(
|
||||
providers_sudo, **kwargs
|
||||
),
|
||||
}
|
||||
payment_context = {
|
||||
'reference_prefix': reference,
|
||||
'amount': amount,
|
||||
'currency': currency,
|
||||
'partner_id': partner_sudo.id,
|
||||
'access_token': access_token,
|
||||
'providers_sudo': providers_sudo,
|
||||
'payment_methods_sudo': payment_methods_sudo,
|
||||
'tokens_sudo': tokens_sudo,
|
||||
'availability_report': availability_report,
|
||||
'transaction_route': '/payment/transaction',
|
||||
'landing_route': '/payment/confirmation',
|
||||
'res_company': company, # Display the correct logo in a multi-company environment
|
||||
'partner_is_different': partner_is_different,
|
||||
**self._get_custom_rendering_context_values(**kwargs),
|
||||
'access_token': access_token,
|
||||
}
|
||||
rendering_context = {
|
||||
**portal_page_values,
|
||||
**payment_form_values,
|
||||
**payment_context,
|
||||
**self._get_extra_payment_form_values(
|
||||
**payment_context, currency_id=currency.id, **kwargs
|
||||
), # Pass the payment context to allow overriding modules to check document access.
|
||||
}
|
||||
return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context)
|
||||
|
||||
@staticmethod
|
||||
def _compute_show_tokenize_input_mapping(providers_sudo, logged_in=False, **kwargs):
|
||||
def _compute_show_tokenize_input_mapping(providers_sudo, **kwargs):
|
||||
""" Determine for each provider whether the tokenization input should be shown or not.
|
||||
|
||||
:param recordset providers_sudo: The providers for which to determine whether the
|
||||
tokenization input should be shown or not, as a sudoed
|
||||
`payment.provider` recordset.
|
||||
:param bool logged_in: Whether the user is logged in or not.
|
||||
:param dict kwargs: The optional data passed to the helper methods.
|
||||
:return: The mapping of the computed value for each provider id.
|
||||
:rtype: dict
|
||||
|
|
@ -165,8 +183,7 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
show_tokenize_input_mapping = {}
|
||||
for provider_sudo in providers_sudo:
|
||||
show_tokenize_input = provider_sudo.allow_tokenization \
|
||||
and not provider_sudo._is_tokenization_required(**kwargs) \
|
||||
and logged_in
|
||||
and not provider_sudo._is_tokenization_required(**kwargs)
|
||||
show_tokenize_input_mapping[provider_sudo.id] = show_tokenize_input
|
||||
return show_tokenize_input_mapping
|
||||
|
||||
|
|
@ -182,43 +199,63 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
:rtype: str
|
||||
"""
|
||||
partner_sudo = request.env.user.partner_id # env.user is always sudoed
|
||||
|
||||
availability_report = {}
|
||||
# Select all the payment methods and tokens that match the payment context.
|
||||
providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
|
||||
request.env.company.id,
|
||||
partner_sudo.id,
|
||||
0., # There is no amount to pay with validation transactions.
|
||||
force_tokenization=True,
|
||||
is_validation=True,
|
||||
)
|
||||
|
||||
# Get all partner's tokens for which providers are not disabled.
|
||||
tokens_sudo = request.env['payment.token'].sudo().search([
|
||||
('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id]),
|
||||
('provider_id.state', 'in', ['enabled', 'test']),
|
||||
])
|
||||
report=availability_report,
|
||||
**kwargs,
|
||||
) # In sudo mode to read the fields of providers and partner (if logged out).
|
||||
payment_methods_sudo = request.env['payment.method'].sudo()._get_compatible_payment_methods(
|
||||
providers_sudo.ids,
|
||||
partner_sudo.id,
|
||||
force_tokenization=True,
|
||||
report=availability_report,
|
||||
) # In sudo mode to read the fields of providers.
|
||||
tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
|
||||
None, partner_sudo.id, is_validation=True
|
||||
) # In sudo mode to read the commercial partner's and providers' fields.
|
||||
|
||||
access_token = payment_utils.generate_access_token(partner_sudo.id, None, None)
|
||||
rendering_context = {
|
||||
'providers': providers_sudo,
|
||||
'tokens': tokens_sudo,
|
||||
|
||||
payment_form_values = {
|
||||
'mode': 'validation',
|
||||
'allow_token_selection': False,
|
||||
'allow_token_deletion': True,
|
||||
}
|
||||
payment_context = {
|
||||
'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'),
|
||||
'partner_id': partner_sudo.id,
|
||||
'access_token': access_token,
|
||||
'providers_sudo': providers_sudo,
|
||||
'payment_methods_sudo': payment_methods_sudo,
|
||||
'tokens_sudo': tokens_sudo,
|
||||
'availability_report': availability_report,
|
||||
'transaction_route': '/payment/transaction',
|
||||
'landing_route': '/my/payment_method',
|
||||
**self._get_custom_rendering_context_values(**kwargs),
|
||||
'access_token': access_token,
|
||||
}
|
||||
rendering_context = {
|
||||
**payment_form_values,
|
||||
**payment_context,
|
||||
**self._get_extra_payment_form_values(**kwargs),
|
||||
}
|
||||
return request.render('payment.payment_methods', rendering_context)
|
||||
|
||||
def _get_custom_rendering_context_values(self, **kwargs):
|
||||
""" Return a dict of additional rendering context values.
|
||||
def _get_extra_payment_form_values(self, **kwargs):
|
||||
""" Return a dict of extra payment form values to include in the rendering context.
|
||||
|
||||
:param dict kwargs: Optional data. This parameter is not used here
|
||||
:return: The dict of additional rendering context values
|
||||
:param dict kwargs: Optional data. This parameter is not used here.
|
||||
:return: The dict of extra payment form values.
|
||||
:rtype: dict
|
||||
"""
|
||||
return {}
|
||||
|
||||
@http.route('/payment/transaction', type='json', auth='public')
|
||||
@http.route('/payment/transaction', type='jsonrpc', auth='public')
|
||||
def payment_transaction(self, amount, currency_id, partner_id, access_token, **kwargs):
|
||||
""" Create a draft transaction and return its processing values.
|
||||
|
||||
|
|
@ -231,58 +268,61 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
:param dict kwargs: Locally unused data passed to `_create_transaction`
|
||||
:return: The mandatory values for the processing of the transaction
|
||||
:rtype: dict
|
||||
:raise: ValidationError if the access token is invalid
|
||||
:raise Forbidden: If the access token is invalid.
|
||||
"""
|
||||
# Check the access token against the transaction values
|
||||
amount = amount and float(amount) # Cast as float in case the JS stripped the '.0'
|
||||
if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
|
||||
raise ValidationError(_("The access token is invalid."))
|
||||
raise Forbidden()
|
||||
|
||||
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
|
||||
self._validate_transaction_kwargs(kwargs, additional_allowed_keys=('reference_prefix',))
|
||||
tx_sudo = self._create_transaction(
|
||||
amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
|
||||
)
|
||||
self._update_landing_route(tx_sudo, access_token) # Add the required parameters to the route
|
||||
self._update_landing_route(tx_sudo, access_token) # Add the required params to the route.
|
||||
return tx_sudo._get_processing_values()
|
||||
|
||||
def _create_transaction(
|
||||
self, payment_option_id, reference_prefix, amount, currency_id, partner_id, flow,
|
||||
tokenization_requested, landing_route, is_validation=False,
|
||||
self, provider_id, payment_method_id, token_id, amount, currency_id, partner_id, flow,
|
||||
tokenization_requested, landing_route, reference_prefix=None, is_validation=False,
|
||||
custom_create_values=None, **kwargs
|
||||
):
|
||||
""" Create a draft transaction based on the payment context and return it.
|
||||
|
||||
:param int payment_option_id: The payment option handling the transaction, as a
|
||||
`payment.provider` id or a `payment.token` id
|
||||
:param str reference_prefix: The custom prefix to compute the full reference
|
||||
:param float|None amount: The amount to pay in the given currency.
|
||||
None if in a payment method validation operation
|
||||
:param int|None currency_id: The currency of the transaction, as a `res.currency` id.
|
||||
None if in a payment method validation operation
|
||||
:param int partner_id: The partner making the payment, as a `res.partner` id
|
||||
:param str flow: The online payment flow of the transaction: 'redirect', 'direct' or 'token'
|
||||
:param bool tokenization_requested: Whether the user requested that a token is created
|
||||
:param str landing_route: The route the user is redirected to after the transaction
|
||||
:param bool is_validation: Whether the operation is a validation
|
||||
:param dict custom_create_values: Additional create values overwriting the default ones
|
||||
:param int provider_id: The provider of the provider payment method or token, as a
|
||||
`payment.provider` id.
|
||||
:param int|None payment_method_id: The payment method, if any, as a `payment.method` id.
|
||||
:param int|None token_id: The token, if any, as a `payment.token` id.
|
||||
:param float|None amount: The amount to pay, or `None` if in a validation operation.
|
||||
:param int|None currency_id: The currency of the amount, as a `res.currency` id, or `None`
|
||||
if in a validation operation.
|
||||
:param int partner_id: The partner making the payment, as a `res.partner` id.
|
||||
:param str flow: The online payment flow of the transaction: 'redirect', 'direct' or 'token'.
|
||||
:param bool tokenization_requested: Whether the user requested that a token is created.
|
||||
:param str landing_route: The route the user is redirected to after the transaction.
|
||||
:param str reference_prefix: The custom prefix to compute the full reference.
|
||||
:param bool is_validation: Whether the operation is a validation.
|
||||
:param dict custom_create_values: Additional create values overwriting the default ones.
|
||||
:param dict kwargs: Locally unused data passed to `_is_tokenization_required` and
|
||||
`_compute_reference`
|
||||
:return: The sudoed transaction that was created
|
||||
:rtype: recordset of `payment.transaction`
|
||||
:raise: UserError if the flow is invalid
|
||||
`_compute_reference`.
|
||||
:return: The sudoed transaction that was created.
|
||||
:rtype: payment.transaction
|
||||
"""
|
||||
# Prepare create values
|
||||
# Prepare the create values.
|
||||
provider_sudo = request.env['payment.provider'].sudo().browse(provider_id)
|
||||
tokenize = False
|
||||
if flow in ['redirect', 'direct']: # Direct payment or payment with redirection
|
||||
provider_sudo = request.env['payment.provider'].sudo().browse(payment_option_id)
|
||||
payment_method_sudo = request.env['payment.method'].sudo().browse(payment_method_id)
|
||||
token_id = None
|
||||
tokenize = bool(
|
||||
# Don't tokenize if the user tried to force it through the browser's developer tools
|
||||
provider_sudo.allow_tokenization
|
||||
and payment_method_sudo.support_tokenization
|
||||
# Token is only created if required by the flow or requested by the user
|
||||
and (provider_sudo._is_tokenization_required(**kwargs) or tokenization_requested)
|
||||
)
|
||||
elif flow == 'token': # Payment by token
|
||||
token_sudo = request.env['payment.token'].sudo().browse(payment_option_id)
|
||||
token_sudo = request.env['payment.token'].sudo().browse(token_id)
|
||||
|
||||
# Prevent from paying with a token that doesn't belong to the current partner (either
|
||||
# the current user's partner if logged in, or the partner on behalf of whom the payment
|
||||
|
|
@ -291,13 +331,7 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
if partner_sudo.commercial_partner_id != token_sudo.partner_id.commercial_partner_id:
|
||||
raise AccessError(_("You do not have access to this payment token."))
|
||||
|
||||
provider_sudo = token_sudo.provider_id
|
||||
token_id = payment_option_id
|
||||
tokenize = False
|
||||
else:
|
||||
raise UserError(
|
||||
_("The payment should either be direct, with redirection, or made by a token.")
|
||||
)
|
||||
payment_method_id = token_sudo.payment_method_id.id
|
||||
|
||||
reference = request.env['payment.transaction']._compute_reference(
|
||||
provider_sudo.code,
|
||||
|
|
@ -307,11 +341,15 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
)
|
||||
if is_validation: # Providers determine the amount and currency in validation operations
|
||||
amount = provider_sudo._get_validation_amount()
|
||||
currency_id = provider_sudo._get_validation_currency().id
|
||||
payment_method = request.env['payment.method'].browse(payment_method_id)
|
||||
currency_id = provider_sudo.with_context(
|
||||
validation_pm=payment_method # Will be converted to a kwarg in master.
|
||||
)._get_validation_currency().id
|
||||
|
||||
# Create the transaction
|
||||
tx_sudo = request.env['payment.transaction'].sudo().create({
|
||||
'provider_id': provider_sudo.id,
|
||||
'payment_method_id': payment_method_id,
|
||||
'reference': reference,
|
||||
'amount': amount,
|
||||
'currency_id': currency_id,
|
||||
|
|
@ -323,13 +361,13 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
**(custom_create_values or {}),
|
||||
}) # In sudo mode to allow writing on callback fields
|
||||
|
||||
if flow == 'token':
|
||||
tx_sudo._send_payment_request() # Payments by token process transactions immediately
|
||||
else:
|
||||
tx_sudo._log_sent_message()
|
||||
if flow != 'token':
|
||||
tx_sudo._log_sent_message() # Direct/Redirect payments go through the payment form.
|
||||
elif not request.env.context.get('delay_token_charge'):
|
||||
tx_sudo._charge_with_token() # Token payments are charged immediately.
|
||||
|
||||
# Monitor the transaction to make it available in the portal
|
||||
PaymentPostProcessing.monitor_transactions(tx_sudo)
|
||||
# Monitor the transaction to make it available in the portal.
|
||||
PaymentPostProcessing.monitor_transaction(tx_sudo)
|
||||
|
||||
return tx_sudo
|
||||
|
||||
|
|
@ -360,7 +398,7 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
:param str tx_id: The transaction to confirm, as a `payment.transaction` id
|
||||
:param str access_token: The access token used to verify the user
|
||||
:param dict kwargs: Optional data. This parameter is not used here
|
||||
:raise: werkzeug.exceptions.NotFound if the access token is invalid
|
||||
:raise NotFound: If the access token is invalid.
|
||||
"""
|
||||
tx_id = self._cast_as_int(tx_id)
|
||||
if tx_id:
|
||||
|
|
@ -370,10 +408,7 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
if not payment_utils.check_access_token(
|
||||
access_token, tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
|
||||
):
|
||||
raise werkzeug.exceptions.NotFound() # Don't leak information about ids.
|
||||
|
||||
# Stop monitoring the transaction now that it reached a final state.
|
||||
PaymentPostProcessing.remove_transactions(tx_sudo)
|
||||
raise NotFound() # Don't leak information about ids.
|
||||
|
||||
# Display the payment confirmation page to the user
|
||||
return request.render('payment.confirm', qcontext={'tx': tx_sudo})
|
||||
|
|
@ -381,7 +416,7 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
# Display the portal homepage to the user
|
||||
return request.redirect('/my/home')
|
||||
|
||||
@http.route('/payment/archive_token', type='json', auth='user')
|
||||
@http.route('/payment/archive_token', type='jsonrpc', auth='user')
|
||||
def archive_token(self, token_id):
|
||||
""" Check that a user has write access on a token and archive the token if so.
|
||||
|
||||
|
|
@ -441,3 +476,36 @@ class PaymentPortal(portal.CustomerPortal):
|
|||
:rtype: str
|
||||
"""
|
||||
return not partner.company_id or partner.company_id == document_company
|
||||
|
||||
@staticmethod
|
||||
def _validate_transaction_kwargs(kwargs, additional_allowed_keys=()):
|
||||
""" Verify that the keys of a transaction route's kwargs are all whitelisted.
|
||||
|
||||
The whitelist consists of all the keys that are expected to be passed to a transaction
|
||||
route, plus optional contextually allowed keys.
|
||||
|
||||
This method must be called in all transaction routes to ensure that no undesired kwarg can
|
||||
be passed as param and then injected in the create values of the transaction.
|
||||
|
||||
:param dict kwargs: The transaction route's kwargs to verify.
|
||||
:param tuple additional_allowed_keys: The keys of kwargs that are contextually allowed.
|
||||
:return: None
|
||||
:raise BadRequest: If some kwargs keys are rejected.
|
||||
"""
|
||||
whitelist = {
|
||||
'provider_id',
|
||||
'payment_method_id',
|
||||
'token_id',
|
||||
'amount',
|
||||
'flow',
|
||||
'tokenization_requested',
|
||||
'landing_route',
|
||||
'is_validation',
|
||||
'csrf_token',
|
||||
}
|
||||
whitelist.update(additional_allowed_keys)
|
||||
rejected_keys = set(kwargs.keys()) - whitelist
|
||||
if rejected_keys:
|
||||
raise BadRequest(
|
||||
_("The following kwargs are not whitelisted: %s", ', '.join(rejected_keys))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import psycopg2
|
||||
|
||||
from odoo import fields, http
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.tools.translate import LazyTranslate
|
||||
|
||||
_lt = LazyTranslate(__name__)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -22,118 +23,70 @@ class PaymentPostProcessing(http.Controller):
|
|||
their post-processing.
|
||||
"""
|
||||
|
||||
MONITORED_TX_IDS_KEY = '__payment_monitored_tx_ids__'
|
||||
MONITORED_TX_ID_KEY = '__payment_monitored_tx_id__'
|
||||
|
||||
@http.route('/payment/status', type='http', auth='public', website=True, sitemap=False)
|
||||
@http.route('/payment/status', type='http', auth='public', website=True, sitemap=False, list_as_website_content=_lt("Payment Status"))
|
||||
def display_status(self, **kwargs):
|
||||
""" Display the payment status page.
|
||||
""" Fetch the transaction and display it on the payment status page.
|
||||
|
||||
:param dict kwargs: Optional data. This parameter is not used here
|
||||
:return: The rendered status page
|
||||
:rtype: str
|
||||
"""
|
||||
return request.render('payment.payment_status')
|
||||
monitored_tx = self._get_monitored_transaction()
|
||||
# The session might have expired, or the transaction never existed.
|
||||
values = {'tx': monitored_tx} if monitored_tx else {'payment_not_found': True}
|
||||
return request.render('payment.payment_status', values)
|
||||
|
||||
@http.route('/payment/status/poll', type='json', auth='public')
|
||||
@http.route('/payment/status/poll', type='jsonrpc', auth='public')
|
||||
def poll_status(self, **_kwargs):
|
||||
""" Fetch the transactions to display on the status page and finalize their post-processing.
|
||||
""" Fetch the transaction and trigger its post-processing.
|
||||
|
||||
:return: The post-processing values of the transactions
|
||||
:return: The post-processing values of the transaction.
|
||||
:rtype: dict
|
||||
"""
|
||||
# Retrieve recent user's transactions from the session
|
||||
limit_date = fields.Datetime.now() - timedelta(days=1)
|
||||
monitored_txs = request.env['payment.transaction'].sudo().search([
|
||||
('id', 'in', self.get_monitored_transaction_ids()),
|
||||
('last_state_change', '>=', limit_date)
|
||||
])
|
||||
if not monitored_txs: # The transaction was not correctly created
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'no_tx_found',
|
||||
}
|
||||
# We only poll the payment status if a payment was found, so the transaction should exist.
|
||||
monitored_tx = self._get_monitored_transaction()
|
||||
|
||||
# Build the list of display values with the display message and post-processing values
|
||||
display_values_list = []
|
||||
for tx in monitored_txs:
|
||||
display_message = None
|
||||
if tx.state == 'pending':
|
||||
display_message = tx.provider_id.pending_msg
|
||||
elif tx.state == 'done':
|
||||
display_message = tx.provider_id.done_msg
|
||||
elif tx.state == 'cancel':
|
||||
display_message = tx.provider_id.cancel_msg
|
||||
display_values_list.append({
|
||||
'display_message': display_message,
|
||||
**tx._get_post_processing_values(),
|
||||
})
|
||||
|
||||
# Stop monitoring already post-processed transactions
|
||||
post_processed_txs = monitored_txs.filtered('is_post_processed')
|
||||
self.remove_transactions(post_processed_txs)
|
||||
|
||||
# Finalize post-processing of transactions before displaying them to the user
|
||||
txs_to_post_process = (monitored_txs - post_processed_txs).filtered(
|
||||
lambda t: t.state == 'done'
|
||||
)
|
||||
success, error = True, None
|
||||
try:
|
||||
txs_to_post_process._finalize_post_processing()
|
||||
except psycopg2.OperationalError: # A collision of accounting sequences occurred
|
||||
request.env.cr.rollback() # Rollback and try later
|
||||
success = False
|
||||
error = 'tx_process_retry'
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
success = False
|
||||
error = str(e)
|
||||
_logger.exception(
|
||||
"encountered an error while post-processing transactions with ids %s:\n%s",
|
||||
', '.join([str(tx_id) for tx_id in txs_to_post_process.ids]), e
|
||||
)
|
||||
# Post-process the transaction before redirecting the user to the landing route and its
|
||||
# document.
|
||||
if not monitored_tx.is_post_processed:
|
||||
try:
|
||||
monitored_tx._post_process()
|
||||
except (
|
||||
psycopg2.OperationalError, psycopg2.IntegrityError
|
||||
): # The database cursor could not be committed.
|
||||
request.env.cr.rollback() # Rollback and try later.
|
||||
raise Exception('retry')
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
_logger.exception(
|
||||
"Encountered an error while post-processing transaction with id %s:\n%s",
|
||||
monitored_tx.id, e
|
||||
)
|
||||
raise
|
||||
|
||||
return {
|
||||
'success': success,
|
||||
'error': error,
|
||||
'display_values_list': display_values_list,
|
||||
'provider_code': monitored_tx.provider_code,
|
||||
'state': monitored_tx.state,
|
||||
'landing_route': monitored_tx.landing_route,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def monitor_transactions(cls, transactions):
|
||||
""" Add the ids of the provided transactions to the list of monitored transaction ids.
|
||||
def monitor_transaction(cls, transaction):
|
||||
""" Make the provided transaction id monitored.
|
||||
|
||||
:param recordset transactions: The transactions to monitor, as a `payment.transaction`
|
||||
recordset
|
||||
:param payment.transaction transaction: The transaction to monitor.
|
||||
:return: None
|
||||
"""
|
||||
if transactions:
|
||||
monitored_tx_ids = request.session.get(cls.MONITORED_TX_IDS_KEY, [])
|
||||
request.session[cls.MONITORED_TX_IDS_KEY] = list(
|
||||
set(monitored_tx_ids).union(transactions.ids)
|
||||
)
|
||||
request.session[cls.MONITORED_TX_ID_KEY] = transaction.id
|
||||
|
||||
@classmethod
|
||||
def get_monitored_transaction_ids(cls):
|
||||
""" Return the ids of transactions being monitored.
|
||||
def _get_monitored_transaction(self):
|
||||
""" Retrieve the user's last transaction from the session (the transaction being monitored).
|
||||
|
||||
Only the ids and not the recordset itself is returned to allow the caller browsing the
|
||||
recordset with sudo privileges, and using the ids in a custom query.
|
||||
|
||||
:return: The ids of transactions being monitored
|
||||
:rtype: list
|
||||
:return: the user's last transaction
|
||||
:rtype: payment.transaction
|
||||
"""
|
||||
return request.session.get(cls.MONITORED_TX_IDS_KEY, [])
|
||||
|
||||
@classmethod
|
||||
def remove_transactions(cls, transactions):
|
||||
""" Remove the ids of the provided transactions from the list of monitored transaction ids.
|
||||
|
||||
:param recordset transactions: The transactions to remove, as a `payment.transaction`
|
||||
recordset
|
||||
:return: None
|
||||
"""
|
||||
if transactions:
|
||||
monitored_tx_ids = request.session.get(cls.MONITORED_TX_IDS_KEY, [])
|
||||
request.session[cls.MONITORED_TX_IDS_KEY] = [
|
||||
tx_id for tx_id in monitored_tx_ids if tx_id not in transactions.ids
|
||||
]
|
||||
return request.env['payment.transaction'].sudo().browse(
|
||||
request.session.get(self.MONITORED_TX_ID_KEY)
|
||||
).exists()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue