예제 #1
0
 def test_get_tx_from_notification_data_creates_refund_tx_when_missing(
         self):
     source_tx = self._create_transaction(
         'direct', state='done', acquirer_reference=self.original_reference)
     data = dict(
         self.webhook_notification_payload,
         amount={
             'currency':
             'USD',
             'value':
             payment_utils.to_minor_currency_units(-self.amount,
                                                   source_tx.currency_id)
         },
         eventCode='REFUND',
     )
     refund_tx = self.env[
         'payment.transaction']._get_tx_from_notification_data(
             'adyen', data)
     self.assertTrue(
         refund_tx,
         msg=
         "If no refund tx is found with received refund data, a refund tx should be created"
     )
     self.assertNotEqual(refund_tx, source_tx)
     self.assertEqual(refund_tx.source_transaction_id, source_tx)
예제 #2
0
    def _stripe_create_payment_intent(self):
        """ Create and return a PaymentIntent.

        :return: The Payment Intent
        :rtype: dict
        """
        if not self.token_id.stripe_payment_method:  # Pre-SCA token -> migrate it
            self.token_id._stripe_sca_migrate_customer()

        payment_intent = self.acquirer_id._stripe_make_request(
            'payment_intents',
            payload={
                'amount':
                payment_utils.to_minor_currency_units(self.amount,
                                                      self.currency_id),
                'currency':
                self.currency_id.name.lower(),
                'confirm':
                True,
                'customer':
                self.token_id.acquirer_ref,
                'off_session':
                True,
                'payment_method':
                self.token_id.stripe_payment_method,
                'description':
                self.reference,
            })
        return payment_intent
예제 #3
0
    def _stripe_prepare_payment_intent_payload(self):
        """ Prepare the payload for the creation of a payment intent in Stripe format.

        Note: This method serves as a hook for modules that would fully implement Stripe Connect.
        Note: self.ensure_one()

        :return: The Stripe-formatted payload for the payment intent request
        :rtype: dict
        """
        return {
            'amount':
            payment_utils.to_minor_currency_units(self.amount,
                                                  self.currency_id),
            'currency':
            self.currency_id.name.lower(),
            'confirm':
            True,
            'customer':
            self.token_id.acquirer_ref,
            'off_session':
            True,
            'payment_method':
            self.token_id.stripe_payment_method,
            'description':
            self.reference,
            'capture_method':
            'manual' if self.acquirer_id.capture_manually else 'automatic',
        }
예제 #4
0
    def _stripe_create_payment_intent(self):
        """ Create and return a PaymentIntent.

        Note: self.ensure_one()

        :return: The Payment Intent
        :rtype: dict
        """
        if not self.token_id.stripe_payment_method:  # Pre-SCA token -> migrate it
            self.token_id._stripe_sca_migrate_customer()

        response = self.acquirer_id._stripe_make_request(
            'payment_intents',
            payload={
                'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id),
                'currency': self.currency_id.name.lower(),
                'confirm': True,
                'customer': self.token_id.acquirer_ref,
                'off_session': True,
                'payment_method': self.token_id.stripe_payment_method,
                'description': self.reference,
            },
            offline=self.operation == 'offline',
        )
        if 'error' not in response:
            payment_intent = response
        else:  # A processing error was returned in place of the payment intent
            error_msg = response['error'].get('message')
            self._set_error("Stripe: " + _(
                "The communication with the API failed.\n"
                "Stripe gave us the following info about the problem:\n'%s'", error_msg
            ))  # Flag transaction as in error now as the intent status might have a valid value
            payment_intent = response['error'].get('payment_intent')  # Get the PI from the error

        return payment_intent
예제 #5
0
 def test_get_tx_from_notification_data_returns_refund_tx(self):
     source_tx = self._create_transaction(
         'direct', state='done', acquirer_reference=self.original_reference)
     refund_tx = self._create_transaction(
         'direct',
         reference='RefundTx',
         acquirer_reference=self.psp_reference,
         amount=-source_tx.amount,
         operation='refund',
         source_transaction_id=source_tx.id)
     data = dict(
         self.webhook_notification_payload,
         amount={
             'currency':
             'USD',
             'value':
             payment_utils.to_minor_currency_units(-source_tx.amount,
                                                   refund_tx.currency_id)
         },
         eventCode='REFUND',
     )
     returned_tx = self.env[
         'payment.transaction']._get_tx_from_notification_data(
             'adyen', data)
     self.assertEqual(returned_tx,
                      refund_tx,
                      msg="The existing refund tx is the one returned")
    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(),
        }
예제 #7
0
 def test_failed_webhook_refund_notification_sets_refund_transaction_in_error(
         self):
     source_tx = self._create_transaction(
         'direct', state='done', acquirer_reference=self.original_reference)
     payload = dict(self.webhook_notification_batch_data,
                    notificationItems=[{
                        'NotificationRequestItem':
                        dict(
                            self.webhook_notification_payload,
                            amount={
                                'currency':
                                'USD',
                                'value':
                                payment_utils.to_minor_currency_units(
                                    -self.amount, source_tx.currency_id)
                            },
                            eventCode='REFUND',
                            success='false',
                        )
                    }])
     self._webhook_notification_flow(payload)
     refund_tx = self.env['payment.transaction'].search([
         ('source_transaction_id', '=', source_tx.id)
     ])
     self.assertEqual(
         refund_tx.state,
         'error',
         msg=
         "After a failed refund notification, the refund state should be in 'error'.",
     )
    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)
예제 #9
0
    def _ogone_send_order_request(self, request_3ds_authentication=False):
        """ Make a new order request to Ogone and return the lxml etree parsed from the response.

        :param bool request_3ds_authentication: Whether a 3DS authentication should be requested if
                                                necessary to process the payment
        :return: The lxml etree
        :raise: ValidationError if the response can not be parsed to an lxml etree
        """
        base_url = self.acquirer_id.get_base_url()
        return_url = urls.url_join(base_url,
                                   OgoneController._directlink_return_url)
        data = {
            # DirectLink parameters
            'PSPID': self.acquirer_id.ogone_pspid,
            'ORDERID': self.reference,
            'USERID': self.acquirer_id.ogone_userid,
            'PSWD': self.acquirer_id.ogone_password,
            'AMOUNT':
            payment_utils.to_minor_currency_units(self.amount, None, 2),
            'CURRENCY': self.currency_id.name,
            'CN': self.partner_name or '',  # Cardholder Name
            'EMAIL': self.partner_email or '',
            'OWNERADDRESS': self.partner_address or '',
            'OWNERZIP': self.partner_zip or '',
            'OWNERTOWN': self.partner_city or '',
            'OWNERCTY': self.partner_country_id.code or '',
            'OWNERTELNO': self.partner_phone or '',
            'OPERATION': 'SAL',  # direct sale
            # Alias Manager parameters
            'ALIAS': self.token_id.acquirer_ref,
            'ALIASPERSISTEDAFTERUSE': 'Y' if self.token_id.active else 'N',
            'ECI': 9,  # Recurring (from eCommerce)
            # 3DS parameters
            'ACCEPTURL': return_url,
            'DECLINEURL': return_url,
            'EXCEPTIONURL': return_url,
            'LANGUAGE': self.partner_lang or 'en_US',
            'FLAG3D': 'Y' if request_3ds_authentication else 'N',
        }
        data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(
            data, incoming=False)

        _logger.info("making payment request:\n%s",
                     pprint.pformat({
                         k: v
                         for k, v in data.items() if k != 'PSWD'
                     }))  # Log the payment request data without the password
        response_content = self.acquirer_id._ogone_make_request(
            'directlink', data)
        try:
            tree = objectify.fromstring(response_content)
        except etree.XMLSyntaxError:
            raise ValidationError(
                "Ogone: " + "Received badly structured response from the API.")
        _logger.info("received payment request response as an etree:\n%s",
                     etree.tostring(tree, pretty_print=True, encoding='utf-8'))
        return tree
예제 #10
0
    def _send_refund_request(self, amount_to_refund=None, create_refund_transaction=True):
        """ Override of payment to send a refund request to Adyen.

        Note: self.ensure_one()

        :param float amount_to_refund: The amount to refund
        :param bool create_refund_transaction: Whether a refund transaction should be created or not
        :return: The refund transaction if any
        :rtype: recordset of `payment.transaction`
        """
        if self.provider != 'adyen':
            return super()._send_refund_request(
                amount_to_refund=amount_to_refund,
                create_refund_transaction=create_refund_transaction
            )
        refund_tx = super()._send_refund_request(
            amount_to_refund=amount_to_refund, create_refund_transaction=True
        )

        # Make the refund request to Adyen
        converted_amount = payment_utils.to_minor_currency_units(
            -refund_tx.amount,  # The amount is negative for refund transactions
            refund_tx.currency_id,
            arbitrary_decimal_number=CURRENCY_DECIMALS.get(refund_tx.currency_id.name)
        )
        data = {
            'merchantAccount': self.acquirer_id.adyen_merchant_account,
            'amount': {
                'value': converted_amount,
                'currency': refund_tx.currency_id.name,
            },
            'reference': refund_tx.reference,
        }
        response_content = refund_tx.acquirer_id._adyen_make_request(
            url_field_name='adyen_checkout_api_url',
            endpoint='/payments/{}/refunds',
            endpoint_param=self.acquirer_reference,
            payload=data,
            method='POST'
        )
        _logger.info(
            "refund request response for transaction with reference %s:\n%s",
            self.reference, pprint.pformat(response_content)
        )

        # Handle the refund request response
        psp_reference = response_content.get('pspReference')
        status = response_content.get('status')
        if psp_reference and status == 'received':
            # The PSP reference associated with this /refunds request is different from the psp
            # reference associated with the original payment request.
            refund_tx.acquirer_reference = psp_reference

        return refund_tx
예제 #11
0
    def _get_specific_rendering_values(self, processing_values):
        """ Override of payment to return Ogone-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.acquirer_id.provider != 'ogone':
            return res

        return_url = urls.url_join(self.acquirer_id.get_base_url(),
                                   OgoneController._return_url)
        rendering_values = {
            'PSPID': self.acquirer_id.ogone_pspid,
            'ORDERID': self.reference,
            'AMOUNT':
            payment_utils.to_minor_currency_units(self.amount, None, 2),
            'CURRENCY': self.currency_id.name,
            'LANGUAGE': self.partner_lang or 'en_US',
            'EMAIL': self.partner_email or '',
            'OWNERADDRESS': self.partner_address or '',
            'OWNERZIP': self.partner_zip or '',
            'OWNERTOWN': self.partner_city or '',
            'OWNERCTY': self.partner_country_id.code or '',
            'OWNERTELNO': self.partner_phone or '',
            'OPERATION': 'SAL',  # direct sale
            'USERID': self.acquirer_id.ogone_userid,
            'ACCEPTURL': return_url,
            'DECLINEURL': return_url,
            'EXCEPTIONURL': return_url,
            'CANCELURL': return_url,
        }
        if self.tokenize:
            rendering_values.update({
                'ALIAS':
                payment_utils.singularize_reference_prefix(
                    prefix='ODOO-ALIAS'),
                'ALIASUSAGE':
                _("Storing your payment details is necessary for future use."),
            })
        rendering_values.update({
            'SHASIGN':
            self.acquirer_id._ogone_generate_signature(rendering_values,
                                                       incoming=False).upper(),
            'api_url':
            self.acquirer_id._ogone_get_api_url('hosted_payment_page'),
        })
        return rendering_values
예제 #12
0
    def _send_refund_request(self):
        """ Override of payment to send a refund request to Authorize.

        Note: self.ensure_one()

        :return: None
        :raise: ValidationError if a badly structured response is received
        """
        super()._send_refund_request()
        if self.provider != 'ogone':
            return

        data = {
            'PSPID': self.acquirer_id.ogone_pspid,
            'ORDERID': self.reference,
            'PAYID': self.acquirer_reference,
            'USERID': self.acquirer_id.ogone_userid,
            'PSWD': self.acquirer_id.ogone_password,
            'AMOUNT':
            payment_utils.to_minor_currency_units(self.amount, None, 2),
            'CURRENCY': self.currency_id.name,
            'OPERATION': 'RFS',  # refund
        }
        data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(
            data, incoming=False)

        _logger.info("making refund request:\n%s",
                     pprint.pformat({
                         k: v
                         for k, v in data.items() if k != 'PSWD'
                     }))  # Log the refund request data without the password
        response_content = self.acquirer_id._ogone_make_request(
            'maintenancedirect', data)
        try:
            tree = objectify.fromstring(response_content)
        except etree.XMLSyntaxError:
            raise ValidationError(
                "Ogone: " + "Received badly structured response from the API.")
        _logger.info("received refund request response as an etree:\n%s",
                     etree.tostring(tree, pretty_print=True, encoding='utf-8'))
        feedback_data = {
            'FEEDBACK_TYPE': 'directlink',
            'ORDERID': tree.get('orderID'),
            'tree': tree,
        }
        _logger.info("entering _handle_feedback_data with data:\n%s",
                     pprint.pformat(feedback_data))
        self._handle_feedback_data('ogone', feedback_data)
예제 #13
0
    def test_redirect_form_values(self):
        """ Test the values of the redirect form inputs for online payments. """
        return_url = self._build_url(OgoneController._hosted_payment_page_return_url)
        expected_values = {
            'PSPID': self.ogone.ogone_pspid,
            'ORDERID': self.reference,
            'AMOUNT': str(payment_utils.to_minor_currency_units(self.amount, None, 2)),
            'CURRENCY': self.currency.name,
            'LANGUAGE': self.partner.lang,
            'EMAIL': self.partner.email,
            'OWNERZIP': self.partner.zip,
            'OWNERADDRESS': payment_utils.format_partner_address(
                self.partner.street, self.partner.street2
            ),
            'OWNERCTY': self.partner.country_id.code,
            'OWNERTOWN': self.partner.city,
            'OWNERTELNO': self.partner.phone,
            'OPERATION': 'SAL',  # direct sale
            'USERID': self.ogone.ogone_userid,
            'ACCEPTURL': return_url,
            'DECLINEURL': return_url,
            'EXCEPTIONURL': return_url,
            'CANCELURL': return_url,
            'ALIAS': None,
            'ALIASUSAGE': None,
        }
        expected_values['SHASIGN'] = self.ogone._ogone_generate_signature(
            expected_values, incoming=False
        ).upper()

        tx = self.create_transaction(flow='redirect')
        self.assertEqual(tx.tokenize, False)
        with mute_logger('odoo.addons.payment.models.payment_transaction'):
            processing_values = tx._get_processing_values()

        form_info = self._extract_values_from_html_form(processing_values['redirect_form_html'])

        self.assertEqual(form_info['action'], 'https://ogone.test.v-psp.com/ncol/test/orderstandard_utf8.asp')
        inputs = form_info['inputs']
        self.assertEqual(len(expected_values), len(inputs))
        for rendering_key, value in expected_values.items():
            form_key = rendering_key.replace('_', '.')
            self.assertEqual(
                inputs[form_key],
                value,
                f"received value {inputs[form_key]} for input {form_key} (expected {value})"
            )
예제 #14
0
    def _send_payment_request(self):
        """ Override of payment to send a payment request to Adyen.

        Note: self.ensure_one()

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

        # Make the payment request to Adyen
        if not self.token_id:
            raise UserError("Adyen: " + _("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)
        )
        data = {
            'merchantAccount': self.acquirer_id.adyen_merchant_account,
            'amount': {
                'value': converted_amount,
                'currency': self.currency_id.name,
            },
            'reference': self.reference,
            'paymentMethod': {
                'recurringDetailReference': self.token_id.acquirer_ref,
            },
            'shopperReference': self.token_id.adyen_shopper_reference,
            'recurringProcessingModel': 'Subscription',
            'shopperIP': payment_utils.get_customer_ip_address(),
            'shopperInteraction': 'ContAuth',
        }
        response_content = self.acquirer_id._adyen_make_request(
            url_field_name='adyen_checkout_api_url',
            endpoint='/payments',
            payload=data,
            method='POST'
        )

        # Handle the payment request response
        _logger.info(
            "payment request response for transaction with reference %s:\n%s",
            self.reference, pprint.pformat(response_content)
        )
        self._handle_feedback_data('adyen', response_content)
예제 #15
0
    def adyen_payment_methods(self,
                              acquirer_id,
                              amount=None,
                              currency_id=None,
                              partner_id=None):
        """ Query the available payment methods based on the transaction context.

        :param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
        :param float amount: The transaction amount
        :param int currency_id: The transaction currency, as a `res.currency` id
        :param int partner_id: The partner making the transaction, as a `res.partner` id
        :return: The JSON-formatted content of the response
        :rtype: dict
        """
        acquirer_sudo = request.env['payment.acquirer'].sudo().browse(
            acquirer_id)
        currency = request.env['res.currency'].browse(currency_id)
        currency_code = currency_id and currency.name
        converted_amount = amount and currency_code and payment_utils.to_minor_currency_units(
            amount, currency, CURRENCY_DECIMALS.get(currency_code))
        partner_sudo = partner_id and request.env['res.partner'].sudo().browse(
            partner_id).exists()
        # The lang is taken from the context rather than from the partner because it is not required
        # to be logged in to make a payment, and because the lang is not always set on the partner.
        # Adyen only supports a limited 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 = (request.context.get('lang') or 'en-US').replace('_', '-')
        shopper_reference = partner_sudo and f'ODOO_PARTNER_{partner_sudo.id}'
        data = {
            'merchantAccount': acquirer_sudo.adyen_merchant_account,
            'amount': converted_amount,
            'countryCode': partner_sudo.country_id.code
            or None,  # ISO 3166-1 alpha-2 (e.g.: 'BE')
            'shopperLocale': lang_code,  # IETF language tag (e.g.: 'fr-BE')
            'shopperReference': shopper_reference,
            'channel': 'Web',
        }
        response_content = acquirer_sudo._adyen_make_request(
            url_field_name='adyen_checkout_api_url',
            endpoint='/paymentMethods',
            payload=data,
            method='POST')
        _logger.info("paymentMethods request response:\n%s",
                     pprint.pformat(response_content))
        return response_content
예제 #16
0
    def _stripe_prepare_payment_intent_payload(self):
        """ Prepare the payload for the creation of a payment intent in Stripe format.

        Note: This method is overridden by the internal module responsible for Stripe Connect.
        Note: self.ensure_one()

        :return: The Stripe-formatted payload for the payment intent request
        :rtype: dict
        """
        return {
            'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id),
            'currency': self.currency_id.name.lower(),
            'confirm': True,
            'customer': self.token_id.acquirer_ref,
            'off_session': True,
            'payment_method': self.token_id.stripe_payment_method,
            'description': self.reference,
        }
예제 #17
0
    def _send_refund_request(self,
                             amount_to_refund=None,
                             create_refund_transaction=True):
        """ Override of payment to send a refund request to Stripe.

        Note: self.ensure_one()

        :param float amount_to_refund: The amount to refund.
        :param bool create_refund_transaction: Whether a refund transaction should be created or
                                               not.
        :return: The refund transaction, if any.
        :rtype: recordset of `payment.transaction`
        """
        if self.provider != 'stripe':
            return super()._send_refund_request(
                amount_to_refund=amount_to_refund,
                create_refund_transaction=create_refund_transaction,
            )
        refund_tx = super()._send_refund_request(
            amount_to_refund=amount_to_refund, create_refund_transaction=True)

        # Make the refund request to stripe.
        data = self.acquirer_id._stripe_make_request(
            'refunds',
            payload={
                'charge':
                self.acquirer_reference,
                'amount':
                payment_utils.to_minor_currency_units(
                    -refund_tx.
                    amount,  # Refund transactions' amount is negative, inverse it.
                    refund_tx.currency_id,
                ),
            })
        _logger.info(
            "Refund request response for transaction wih reference %s:\n%s",
            self.reference, pprint.pformat(data))
        # Handle the refund request response.
        notification_data = {}
        StripeController._include_refund_in_notification_data(
            data, notification_data)
        refund_tx._handle_notification_data('stripe', notification_data)

        return refund_tx
예제 #18
0
    def _get_specific_rendering_values(self, processing_values):
        """ Override of payment to return Sips-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 != 'sips':
            return res

        base_url = self.get_base_url()
        data = {
            'amount':
            payment_utils.to_minor_currency_units(self.amount,
                                                  self.currency_id),
            'currencyCode':
            SUPPORTED_CURRENCIES[self.currency_id.name],  # The ISO 4217 code
            'merchantId':
            self.acquirer_id.sips_merchant_id,
            'normalReturnUrl':
            urls.url_join(base_url, SipsController._return_url),
            'automaticResponseUrl':
            urls.url_join(base_url, SipsController._webhook_url),
            'transactionReference':
            self.reference,
            'statementReference':
            self.reference,
            'keyVersion':
            self.acquirer_id.sips_key_version,
            'returnContext':
            json.dumps(dict(reference=self.reference)),
        }
        api_url = self.acquirer_id.sips_prod_url if self.acquirer_id.state == 'enabled' \
            else self.acquirer_id.sips_test_url
        data = '|'.join([f'{k}={v}' for k, v in data.items()])
        return {
            'api_url': api_url,
            'Data': data,
            'InterfaceVersion': self.acquirer_id.sips_version,
            'Seal': self.acquirer_id._sips_generate_shasign(data),
        }
예제 #19
0
    def test_processing_values(self):
        tx = self.create_transaction(flow='direct')
        with mute_logger('odoo.addons.payment.models.payment_transaction'), \
            patch(
                'odoo.addons.payment.utils.generate_access_token',
                new=self._generate_test_access_token
            ):
            processing_values = tx._get_processing_values()

        converted_amount = 111111
        self.assertEqual(
            payment_utils.to_minor_currency_units(self.amount, self.currency),
            converted_amount,
        )
        self.assertEqual(processing_values['converted_amount'],
                         converted_amount)
        with patch('odoo.addons.payment.utils.generate_access_token',
                   new=self._generate_test_access_token):
            self.assertTrue(
                payment_utils.check_access_token(
                    processing_values['access_token'], self.reference,
                    converted_amount, self.partner.id))
예제 #20
0
    def _verify_notification_signature(self, received_signature, tx):
        """ Check that the signature computed from the transaction values matches the received one.

        :param str received_signature: The signature sent with the notification
        :param recordset tx: The transaction of the notification, as a `payment.transaction` record
        :return: Whether the signatures match
        :rtype: str
        """

        if not received_signature:
            _logger.warning("ignored notification with missing signature")
            return False

        converted_amount = payment_utils.to_minor_currency_units(
            tx.amount, tx.currency_id,
            CURRENCY_DECIMALS.get(tx.currency_id.name))
        if not payment_utils.check_access_token(
                received_signature, converted_amount, tx.currency_id.name,
                tx.reference):
            _logger.warning("ignored notification with invalid signature")
            return False

        return True
예제 #21
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'])
        }
예제 #22
0
    def _stripe_create_checkout_session(self):
        """ Create and return a Checkout Session.

        :return: The Checkout Session
        :rtype: dict
        """
        # Filter payment method types by available payment method
        existing_pms = [
            pm.name.lower() for pm in self.env['payment.icon'].search([])
        ]
        linked_pms = [
            pm.name.lower() for pm in self.acquirer_id.payment_icon_ids
        ]
        pm_filtered_pmts = filter(
            lambda pmt: pmt.name == 'card'
            # If the PM (payment.icon) record related to a PMT doesn't exist, don't filter out the
            # PMT because the user couldn't even have linked it to the acquirer in the first place.
            or (pmt.name in linked_pms or pmt.name not in existing_pms),
            PAYMENT_METHOD_TYPES)
        # Filter payment method types by country code
        country_code = self.partner_country_id and self.partner_country_id.code.lower(
        )
        country_filtered_pmts = filter(
            lambda pmt: not pmt.countries or country_code in pmt.countries,
            pm_filtered_pmts)
        # Filter payment method types by currency name
        currency_name = self.currency_id.name.lower()
        currency_filtered_pmts = filter(
            lambda pmt: not pmt.currencies or currency_name in pmt.currencies,
            country_filtered_pmts)
        # Filter payment method types by recurrence if the transaction must be tokenized
        if self.tokenize:
            recurrence_filtered_pmts = filter(
                lambda pmt: pmt.recurrence == 'recurring',
                currency_filtered_pmts)
        else:
            recurrence_filtered_pmts = currency_filtered_pmts
        # Build the session values related to payment method types
        pmt_values = {}
        for pmt_id, pmt_name in enumerate(
                map(lambda pmt: pmt.name, recurrence_filtered_pmts)):
            pmt_values[f'payment_method_types[{pmt_id}]'] = pmt_name

        # Create the session according to the operation and return it
        customer = self._stripe_create_customer()
        common_session_values = self._get_common_stripe_session_values(
            pmt_values, customer)
        base_url = self.acquirer_id.get_base_url()
        if self.operation == 'online_redirect':
            return_url = f'{urls.url_join(base_url, StripeController._checkout_return_url)}' \
                         f'?reference={urls.url_quote_plus(self.reference)}'
            # Specify a future usage for the payment intent to:
            # 1. attach the payment method to the created customer
            # 2. trigger a 3DS check if one if required, while the customer is still present
            future_usage = 'off_session' if self.tokenize else None
            capture_method = 'manual' if self.acquirer_id.capture_manually else 'automatic'
            checkout_session = self.acquirer_id._stripe_make_request(
                'checkout/sessions',
                payload={
                    **common_session_values,
                    'mode':
                    'payment',
                    'success_url':
                    return_url,
                    'cancel_url':
                    return_url,
                    'line_items[0][price_data][currency]':
                    self.currency_id.name,
                    'line_items[0][price_data][product_data][name]':
                    self.reference,
                    'line_items[0][price_data][unit_amount]':
                    payment_utils.to_minor_currency_units(
                        self.amount, self.currency_id),
                    'line_items[0][quantity]':
                    1,
                    'payment_intent_data[description]':
                    self.reference,
                    'payment_intent_data[setup_future_usage]':
                    future_usage,
                    'payment_intent_data[capture_method]':
                    capture_method,
                })
            self.stripe_payment_intent = checkout_session['payment_intent']
        else:  # 'validation'
            # {CHECKOUT_SESSION_ID} is a template filled by Stripe when the Session is created
            return_url = f'{urls.url_join(base_url, StripeController._validation_return_url)}' \
                         f'?reference={urls.url_quote_plus(self.reference)}' \
                         f'&checkout_session_id={{CHECKOUT_SESSION_ID}}'
            checkout_session = self.acquirer_id._stripe_make_request(
                'checkout/sessions',
                payload={
                    **common_session_values,
                    'mode': 'setup',
                    'success_url': return_url,
                    'cancel_url': return_url,
                    'setup_intent_data[description]': self.reference,
                })
        return checkout_session
예제 #23
0
    def _send_payment_request(self):
        """ Override of payment to send a payment request to Ogone.

        Note: self.ensure_one()

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

        if not self.token_id:
            raise UserError("Ogone: " +
                            _("The transaction is not linked to a token."))

        # Make the payment request
        base_url = self.acquirer_id.get_base_url()
        data = {
            # DirectLink parameters
            'PSPID': self.acquirer_id.ogone_pspid,
            'ORDERID': self.reference,
            'USERID': self.acquirer_id.ogone_userid,
            'PSWD': self.acquirer_id.ogone_password,
            'AMOUNT':
            payment_utils.to_minor_currency_units(self.amount, None, 2),
            'CURRENCY': self.currency_id.name,
            'CN': self.partner_name or '',  # Cardholder Name
            'EMAIL': self.partner_email or '',
            'OWNERADDRESS': self.partner_address or '',
            'OWNERZIP': self.partner_zip or '',
            'OWNERTOWN': self.partner_city or '',
            'OWNERCTY': self.partner_country_id.code or '',
            'OWNERTELNO': self.partner_phone or '',
            'OPERATION': 'SAL',  # direct sale
            # Alias Manager parameters
            'ALIAS': self.token_id.acquirer_ref,
            'ALIASPERSISTEDAFTERUSE': 'Y',
            'ECI': 9,  # Recurring (from eCommerce)
        }
        data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(
            data, incoming=False)

        _logger.info("making payment request:\n%s",
                     pprint.pformat({
                         k: v
                         for k, v in data.items() if k != 'PSWD'
                     }))  # Log the payment request data without the password
        response_content = self.acquirer_id._ogone_make_request(data)
        try:
            tree = objectify.fromstring(response_content)
        except etree.XMLSyntaxError:
            raise ValidationError(
                "Ogone: " + "Received badly structured response from the API.")

        # Handle the feedback data
        _logger.info("received payment request response as an etree:\n%s",
                     etree.tostring(tree, pretty_print=True, encoding='utf-8'))
        feedback_data = {'ORDERID': tree.get('orderID'), 'tree': tree}
        _logger.info("entering _handle_feedback_data with data:\n%s",
                     pprint.pformat(feedback_data))
        self._handle_feedback_data('ogone', feedback_data)