def _compute_values(self):
     for payment_link in self:
         payment_link.access_token = payment_utils.generate_access_token(
             payment_link.partner_id.id, payment_link.amount,
             payment_link.currency_id.id)
     # must be called after token generation, obvsly - the link needs an up-to-date token
     self._generate_link()
Example #2
0
    def payment_method(self, **kwargs):
        """ Display the form to manage payment methods.

        :param dict kwargs: Optional data. This parameter is not used here
        :return: The rendered manage form
        :rtype: str
        """
        partner = request.env.user.partner_id
        acquirers_sudo = request.env['payment.acquirer'].sudo()._get_compatible_acquirers(
            request.env.company.id, partner.id, force_tokenization=True
        )
        tokens = set(partner.payment_token_ids).union(
            partner.commercial_partner_id.sudo().payment_token_ids
        )  # Show all partner's tokens, regardless of which acquirer is available
        access_token = payment_utils.generate_access_token(partner.id, None, None)
        rendering_context = {
            'acquirers': acquirers_sudo,
            'tokens': tokens,
            'reference_prefix': payment_utils.singularize_reference_prefix(prefix='validation'),
            'partner_id': partner.id,
            'access_token': access_token,
            'transaction_route': '/payment/transaction',
            'validation_route': '/payment/validation',
            'landing_route': '/my/payment_method',
            **self._get_custom_rendering_context_values(**kwargs),
        }
        return request.render('payment.payment_methods', rendering_context)
    def _get_specific_rendering_values(self, processing_values):
        """ Override of payment to return Odoo-specific rendering values.

        Note: self.ensure_one() from `_get_processing_values`

        :param dict processing_values: The generic and specific processing values of the transaction
        :return: The dict of acquirer-specific processing values
        :rtype: dict
        """
        res = super()._get_specific_rendering_values(processing_values)
        if self.provider != 'odoo':
            return res

        converted_amount = payment_utils.to_minor_currency_units(
            self.amount, self.currency_id,
            CURRENCY_DECIMALS.get(self.currency_id.name))
        # The lang is taken from the context rather than from the partner because it is not required
        # to be logged to make a payment and because the lang is not always set on the partner.
        # Adyen only supports a reduced set of languages but, instead of looking for the closest
        # match in https://docs.adyen.com/checkout/components-web/localization-components, we simply
        # provide the lang string as is (after adapting the format) and let Adyen find the best fit.
        lang_code = (self._context.get('lang') or 'en-US').replace('_', '-')
        base_url = self.acquirer_id._get_base_url()
        signature = payment_utils.generate_access_token(
            converted_amount, self.currency_id.name, self.reference)
        data = {
            'adyen_uuid':
            self.acquirer_id.odoo_adyen_account_id.adyen_uuid,
            'payout':
            self.acquirer_id.odoo_adyen_payout_id.code,
            'amount': {
                'value': converted_amount,
                'currency': self.currency_id.name,
            },
            'reference':
            self.reference,
            'shopperLocale':
            lang_code,
            'shopperReference':
            self.acquirer_id._odoo_compute_shopper_reference(
                self.partner_id.id),
            'recurringProcessingModel':
            'CardOnFile',
            'storePaymentMethod':
            self.tokenize,  # True by default on Adyen side
            # Since the Pay by Link API redirects the customer without any payload, we use the
            # /payment/status route directly as return url.
            'returnUrl':
            urls.url_join(base_url, '/payment/status'),
            'metadata': {
                'merchant_signature':
                signature,
                'notification_url':
                urls.url_join(base_url, OdooController._notification_url),
            },  # Proxy-specific data
        }
        return {
            'data': json.dumps(data),
            'api_url': self.acquirer_id._odoo_get_api_url(),
        }
Example #4
0
    def payment_method(self, **kwargs):
        """ Display the form to manage payment methods.

        :param dict kwargs: Optional data. This parameter is not used here
        :return: The rendered manage form
        :rtype: str
        """
        partner_sudo = request.env.user.partner_id  # env.user is always sudoed
        acquirers_sudo = request.env['payment.acquirer'].sudo()._get_compatible_acquirers(
            request.env.company.id, partner_sudo.id, force_tokenization=True, is_validation=True
        )

        # Get all partner's tokens for which acquirers are not disabled.
        tokens_sudo = request.env['payment.token'].sudo().search([
            ('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id]),
            ('acquirer_id.state', 'in', ['enabled', 'test']),
        ])

        access_token = payment_utils.generate_access_token(partner_sudo.id, None, None)
        rendering_context = {
            'acquirers': acquirers_sudo,
            'tokens': tokens_sudo,
            'reference_prefix': payment_utils.singularize_reference_prefix(prefix='validation'),
            'partner_id': partner_sudo.id,
            'access_token': access_token,
            'transaction_route': '/payment/transaction',
            'landing_route': '/my/payment_method',
            **self._get_custom_rendering_context_values(**kwargs),
        }
        return request.render('payment.payment_methods', rendering_context)
    def _send_payment_request(self):
        """ Override of payment to send a payment request to Adyen through the Odoo proxy.

        Note: self.ensure_one()

        :return: None
        :raise: UserError if the transaction is not linked to a token
        """
        super()._send_payment_request()
        if self.provider != 'odoo':
            return

        # Make the payment request
        if not self.token_id:
            raise UserError("Odoo Payments: " +
                            _("The transaction is not linked to a token."))

        converted_amount = payment_utils.to_minor_currency_units(
            self.amount, self.currency_id,
            CURRENCY_DECIMALS.get(self.currency_id.name))
        base_url = self.acquirer_id._get_base_url()
        signature = payment_utils.generate_access_token(
            converted_amount, self.currency_id.name, self.reference)
        data = {
            'payout':
            self.acquirer_id.odoo_adyen_payout_id.code,
            'amount': {
                'value': converted_amount,
                'currency': self.currency_id.name,
            },
            'reference':
            self.reference,
            'paymentMethod': {
                'type': self.token_id.odoo_payment_method_type,
                'storedPaymentMethodId': self.token_id.acquirer_ref,
            },
            'shopperReference':
            self.acquirer_id._odoo_compute_shopper_reference(
                self.partner_id.id),
            'recurringProcessingModel':
            'Subscription',
            'shopperInteraction':
            'ContAuth',
            'metadata': {
                'merchant_signature':
                signature,
                'notification_url':
                urls.url_join(base_url, OdooController._notification_url),
            },  # Proxy-specific data
        }
        response_content = self.acquirer_id.odoo_adyen_account_id._adyen_rpc(
            'payments', data)

        # Handle the payment request response
        _logger.info("payment request response:\n%s",
                     pprint.pformat(response_content))
        self._handle_feedback_data('odoo', response_content)
Example #6
0
    def _update_landing_route(tx_sudo, access_token):
        """ Add the mandatory parameters to the route and recompute the access token if needed.

        The generic landing route requires the tx id and access token to be provided since there is
        no document to rely on. The access token is recomputed in case we are dealing with a
        validation transaction (acquirer-specific amount and currency).

        :param recordset tx_sudo: The transaction whose landing routes to update, as a
                                  `payment.transaction` record.
        :param str access_token: The access token used to authenticate the partner
        :return: None
        """
        if tx_sudo.operation == 'validation':
            access_token = payment_utils.generate_access_token(
                tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id)
        tx_sudo.landing_route = f'{tx_sudo.landing_route}' \
                                f'?tx_id={tx_sudo.id}&access_token={access_token}'
Example #7
0
    def _get_specific_processing_values(self, processing_values):
        """ Override of payment to return an access token as acquirer-specific processing values.

        Note: self.ensure_one() from `_get_processing_values`

        :param dict processing_values: The generic processing values of the transaction
        :return: The dict of acquirer-specific processing values
        :rtype: dict
        """
        res = super()._get_specific_processing_values(processing_values)
        if self.provider != 'authorize':
            return res

        return {
            'access_token': payment_utils.generate_access_token(
                processing_values['reference'], processing_values['partner_id']
            )
        }
Example #8
0
    def payment_transaction(self, amount, currency_id, partner_id,
                            access_token, **kwargs):
        """ Create a draft transaction and return its processing values.

        :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 access_token: The access token used to authenticate the partner
        :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
        """
        # 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."))

        kwargs.pop('custom_create_values',
                   None)  # Don't allow passing arbitrary create values
        tx_sudo = self._create_transaction(amount=amount,
                                           currency_id=currency_id,
                                           partner_id=partner_id,
                                           **kwargs)

        # The generic validation and landing routes require the tx id and access token to be
        # provided, since there is no document to rely on. The access token is recomputed in case
        # we are dealing with a validation transaction (acquirer-specific amount and currency).
        access_token = payment_utils.generate_access_token(
            tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id)
        tx_sudo.validation_route = tx_sudo.validation_route \
                                   and f'{tx_sudo.validation_route}&access_token={access_token}'
        tx_sudo.landing_route = f'{tx_sudo.landing_route}' \
                                f'?tx_id={tx_sudo.id}&access_token={access_token}'

        return tx_sudo._get_processing_values()
Example #9
0
    def donation_pay(self, **kwargs):
        """ Behaves like PaymentPortal.payment_pay but for donation

        :param dict kwargs: As the parameters of in payment_pay, with the additional:
            - str donation_options: The options settled in the donation snippet
            - str donation_descriptions: The descriptions for all prefilled amounts
        :return: The rendered donation form
        :rtype: str
        :raise: werkzeug.exceptions.NotFound if the access token is invalid
        """
        kwargs['is_donation'] = True
        kwargs['currency_id'] = int(
            kwargs.get('currency_id', request.env.company.currency_id.id))
        kwargs['amount'] = float(kwargs.get('amount', 25))
        kwargs['donation_options'] = kwargs.get(
            'donation_options',
            json_safe.dumps(dict(customAmount="freeAmount")))

        if request.env.user._is_public():
            kwargs['partner_id'] = request.env.user.partner_id.id
            kwargs['access_token'] = payment_utils.generate_access_token(
                kwargs['partner_id'], kwargs['amount'], kwargs['currency_id'])

        return self.payment_pay(**kwargs)
Example #10
0
    def _get_specific_processing_values(self, processing_values):
        """ Override of payment to return Adyen-specific processing values.

        Note: self.ensure_one() from `_get_processing_values`

        :param dict processing_values: The generic processing values of the transaction
        :return: The dict of acquirer-specific processing values
        :rtype: dict
        """
        res = super()._get_specific_processing_values(processing_values)
        if self.provider != 'adyen':
            return res

        converted_amount = payment_utils.to_minor_currency_units(
            self.amount, self.currency_id,
            CURRENCY_DECIMALS.get(self.currency_id.name))
        return {
            'converted_amount':
            converted_amount,
            'access_token':
            payment_utils.generate_access_token(
                processing_values['reference'], converted_amount,
                processing_values['partner_id'])
        }
Example #11
0
    def payment_pay(
        self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None,
        acquirer_id=None, access_token=None, **kwargs
    ):
        """ Display the payment form with optional filtering of payment options.

        The filtering takes place on the basis of provided parameters, if any. If a parameter is
        incorrect or malformed, it is skipped to avoid preventing the user from making the payment.

        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 acquirer_id: The desired acquirer, as a `payment.acquirer` id
        :param str access_token: The access token used to authenticate the partner
        :param dict kwargs: Optional data. This parameter is not used here
        :return: The rendered checkout form
        :rtype: str
        :raise: werkzeug.exceptions.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, acquirer_id, partner_id, company_id = tuple(map(
            self.cast_as_int, (currency_id, acquirer_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 info about the existence of an id

        user_sudo = request.env.user
        logged_in = not user_sudo._is_public()
        # If the user is logged in, take their partner rather than the partner set in the params.
        # This is something that we want, since security rules are based on the partner, and created
        # tokens should not be assigned to the public user. This should have no impact on the
        # transaction itself besides making reconciliation possibly more difficult (e.g. The
        # transaction and invoice partners are different).
        partner_is_different = False
        if logged_in:
            partner_is_different = partner_id and partner_id != user_sudo.partner_id.id
            partner_sudo = user_sudo.partner_id
        else:
            partner_sudo = request.env['res.partner'].sudo().browse(partner_id).exists()
            if not partner_sudo:
                return request.redirect(
                    # Escape special characters to avoid loosing original params when redirected
                    f'/web/login?redirect={urllib.parse.quote(request.httprequest.full_path)}'
                )

        # Instantiate transaction values to their default if not set in parameters
        reference = reference or payment_utils.singularize_reference_prefix(prefix='tx')
        amount = amount or 0.0  # If the amount is invalid, set it to 0 to stop the payment flow
        company_id = company_id or partner_sudo.company_id.id or user_sudo.company_id.id
        currency_id = currency_id or request.env['res.company'].browse(company_id).currency_id.id

        # 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

        # Select all acquirers and tokens that match the constraints
        acquirers_sudo = request.env['payment.acquirer'].sudo()._get_compatible_acquirers(
            company_id, partner_sudo.id, currency_id=currency.id
        )  # In sudo mode to read the fields of acquirers and partner (if not logged in)
        if acquirer_id in acquirers_sudo.ids:  # Only keep the desired acquirer if it's suitable
            acquirers_sudo = acquirers_sudo.browse(acquirer_id)
        payment_tokens = request.env['payment.token'].search(
            [('acquirer_id', 'in', acquirers_sudo.ids), ('partner_id', '=', partner_sudo.id)]
        ) if logged_in else request.env['payment.token']

        # Compute the fees taken by acquirers supporting the feature
        fees_by_acquirer = {
            acq_sudo: acq_sudo._compute_fees(amount, currency, partner_sudo.country_id)
            for acq_sudo in acquirers_sudo.filtered('fees_active')
        }

        # 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 = {
            'acquirers': acquirers_sudo,
            'tokens': payment_tokens,
            'fees_by_acquirer': fees_by_acquirer,
            'show_tokenize_input': logged_in,  # Prevent public partner from saving payment methods
            'reference_prefix': reference,
            'amount': amount,
            'currency': currency,
            'partner_id': partner_sudo.id,
            'access_token': access_token,
            'transaction_route': '/payment/transaction',
            'landing_route': '/payment/confirmation',
            'partner_is_different': partner_is_different,
            **self._get_custom_rendering_context_values(**kwargs),
        }
        return request.render('payment.pay', rendering_context)