예제 #1
0
    def test_capturable_payment_that_shouldnt_be_captured_yet_with_email_already_set(
            self, mock_send_email):
        """
        Test that if the govuk payment is in 'capturable' state, the MTP payment record
        has already the email field filled in and the payment should not be captured yet:

        - the method returns GovUkPaymentStatus.capturable
        - no email is sent as it
        """
        client = PaymentClient()

        payment = {
            'uuid': 'some-id',
            'email': '*****@*****.**',
            'worldpay_id': '123456789',
            'cardholder_name': 'John Doe',
            'card_number_first_digits': '1234',
            'card_number_last_digits': '987',
            'card_expiry_date': '01/20',
            'card_brand': 'visa',
            'billing_address': {
                'line1': '102 Petty France',
                'line2': '',
                'postcode': 'SW1H9AJ',
                'city': 'London',
                'country': 'GB',
            },
            'security_check': {
                'status': 'pending',
                'user_actioned': False,
            },
        }
        govuk_payment = {
            'payment_id': 'payment-id',
            'state': {
                'status': GovUkPaymentStatus.capturable.name,
            },
            'email': '*****@*****.**',
            'provider_id': '123456789',
            'card_details': {
                'cardholder_name': 'John Doe',
                'first_digits_card_number': '1234',
                'last_digits_card_number': '987',
                'expiry_date': '01/20',
                'card_brand': 'visa',
                'billing_address': {
                    'line1': '102 Petty France',
                    'line2': '',
                    'postcode': 'SW1H9AJ',
                    'city': 'London',
                    'country': 'GB',
                },
            },
        }

        status = client.complete_payment_if_necessary(payment, govuk_payment)

        self.assertEqual(status, GovUkPaymentStatus.capturable)
        mock_send_email.assert_not_called()
예제 #2
0
    def test_do_nothing_if_govukpayment_is_falsy(self, mock_send_email):
        """
        Test that if the passed in govuk payment dict is falsy, the method returns None and
        doesn't send any email.
        """
        client = PaymentClient()

        payment = {}
        govuk_payment = {}
        status = client.complete_payment_if_necessary(payment, govuk_payment)

        self.assertEqual(status, None)
        mock_send_email.assert_not_called()
예제 #3
0
    def test_dont_send_email(self, mock_send_email):
        """
        Test that the method only sends any email if the govuk payment status is 'capturable'
        and the MTP payment didn't have the email field set
        """
        client = PaymentClient()

        payment = {
            'uuid': 'some-id',
        }

        statuses = [
            status for status in GovUkPaymentStatus
            if status != GovUkPaymentStatus.capturable
        ]

        with responses.RequestsMock() as rsps, silence_logger():
            mock_auth(rsps)

            # API call related to updating the email address on the payment record
            rsps.add(
                rsps.PATCH,
                api_url(f'/payments/{payment["uuid"]}/'),
                json={
                    'email': '*****@*****.**',
                },
                status=200,
            )

            for status in statuses:
                govuk_payment = {
                    'payment_id': 'payment-id',
                    'state': {
                        'status': status.name,

                        # for status == 'errors'
                        'code': 'code',
                        'message': 'message',
                    },
                    'email': '*****@*****.**',
                }
                actual_status = client.complete_payment_if_necessary(
                    payment, govuk_payment)

                self.assertEqual(actual_status, status)
                mock_send_email.assert_not_called()
예제 #4
0
    def perform_update(self):
        payment_client = PaymentClient()
        payments = payment_client.get_incomplete_payments()
        for payment in payments:
            if not self.should_be_checked(payment):
                continue

            payment_ref = payment['uuid']
            govuk_id = payment['processor_id']

            try:
                govuk_payment = payment_client.get_govuk_payment(govuk_id)
                previous_govuk_status = GovUkPaymentStatus.get_from_govuk_payment(
                    govuk_payment)
                govuk_status = payment_client.complete_payment_if_necessary(
                    payment, govuk_payment)

                # not yet finished and can't do anything so skip
                if govuk_status and not govuk_status.finished():
                    continue

                if previous_govuk_status != govuk_status:
                    # refresh govuk payment to get up-to-date fields (e.g. error codes)
                    govuk_payment = payment_client.get_govuk_payment(govuk_id)

                # if here, status is either success, failed, cancelled, error
                # or None (in case of govuk payment not found)
                payment_client.update_completed_payment(payment, govuk_payment)
            except OAuth2Error:
                logger.exception(
                    'Scheduled job: Authentication error while processing %s' %
                    payment_ref)
            except RequestException as error:
                error_message = 'Scheduled job: Payment check failed for ref %s' % payment_ref
                if hasattr(error, 'response') and hasattr(
                        error.response, 'content'):
                    error_message += '\nReceived: %s' % error.response.content
                logger.exception(error_message)
            except GovUkPaymentStatusException:
                # expected much of the time
                pass
    def get(self, request, *args, **kwargs):
        payment_ref = self.request.GET.get('payment_ref')
        if not payment_ref:
            return clear_session_view(request)
        kwargs['short_payment_ref'] = payment_ref[:8].upper()
        try:
            # check payment status
            payment_client = PaymentClient()
            payment = payment_client.get_payment(payment_ref)

            # only continue if:
            # - the MTP payment is in pending (it moves to the 'taken' state by the cronjob x mins after
            #   the gov.uk payment succeeds)
            #   OR
            # - the MTP payment is in the 'taken' state (by the cronjob x mins after the gov.uk payment succeeded)
            #   but only for a limited period of time
            if not payment or not is_active_payment(payment):
                return clear_session_view(request)

            kwargs.update({
                'prisoner_name': payment['recipient_name'],
                'prisoner_number': payment['prisoner_number'],
                'amount': decimal.Decimal(payment['amount']) / 100,
            })

            if payment['status'] == 'taken':
                self.status = GovUkPaymentStatus.success
            else:
                # check gov.uk payment status
                govuk_id = payment['processor_id']
                govuk_payment = payment_client.get_govuk_payment(govuk_id)

                self.status = payment_client.complete_payment_if_necessary(
                    payment, govuk_payment)

                # here status can be either created, started, submitted, capturable, success, failed, cancelled, error
                # or None

                error_code = govuk_payment and govuk_payment.get(
                    'state', {}).get('code')

                # payment was cancelled programmatically (this would not currently happen)
                if self.status == GovUkPaymentStatus.cancelled:
                    # error_code is expected to be P0040
                    error_code == 'P0040' or logger.error(
                        f'Unexpected code for cancelled GOV.UK Pay payment {payment_ref}: {error_code}'
                    )
                    return render(request,
                                  'send_money/debit-card-cancelled.html')

                # the user cancelled the payment
                if self.status == GovUkPaymentStatus.failed and error_code == 'P0030':
                    return render(request,
                                  'send_money/debit-card-cancelled.html')

                # GOV.UK Pay session expired
                if self.status == GovUkPaymentStatus.failed and error_code == 'P0020':
                    return render(
                        request, 'send_money/debit-card-session-expired.html')

                # payment method was rejected by card issuer or processor
                # e.g. due to insufficient funds or risk management
                if self.status == GovUkPaymentStatus.failed:
                    # error_code is expected to be P0010
                    error_code == 'P0010' or logger.error(
                        f'Unexpected code for failed GOV.UK Pay payment {payment_ref}: {error_code}'
                    )
                    return render(request,
                                  'send_money/debit-card-declined.html')

                # here status can be either created, started, submitted, capturable, success, error
                # or None

                # treat statuses created, started, submitted or None as error as they should have never got here
                if not self.status or self.status.is_awaiting_user_input():
                    self.status = GovUkPaymentStatus.error

                # here status can be either capturable, success, error

        except OAuth2Error:
            logger.exception(
                'Authentication error while processing %(payment_ref)s',
                {'payment_ref': payment_ref},
            )
            self.status = GovUkPaymentStatus.error
        except RequestException as error:
            response_content = get_requests_exception_for_logging(error)
            logger.exception(
                'Payment check failed for ref %(payment_ref)s. Received: %(response_content)s',
                {
                    'payment_ref': payment_ref,
                    'response_content': response_content
                },
            )
            self.status = GovUkPaymentStatus.error
        except GovUkPaymentStatusException:
            logger.exception(
                'GOV.UK Pay returned unexpected status for ref %(payment_ref)s',
                {'payment_ref': payment_ref},
            )
            self.status = GovUkPaymentStatus.error

        response = super().get(request, *args, **kwargs)
        request.session.flush()
        return response
예제 #6
0
    def test_capturable_payment_that_should_be_cancelled(
            self, mock_send_email):
        """
        Test that if the govuk payment is in 'capturable' state and the payment should be cancelled:

        - the MTP payment record is patched with the card details attributes if necessary
        - the method cancels the payment
        - no email is sent
        - the method returns GovUkPaymentStatus.cancelled
        """
        client = PaymentClient()

        payment = {
            'uuid': 'some-id',
            'recipient_name': 'Alice Re',
            'prisoner_number': 'AAB0A00',
            'prisoner_name': 'John Doe',
            'amount': 1700,
            'security_check': {
                'status': 'rejected',
                'user_actioned': True,
            },
        }
        payment_extra_details = {
            'email': '*****@*****.**',
            'worldpay_id': '123456789',
            'cardholder_name': 'John Doe',
            'card_number_first_digits': '1234',
            'card_number_last_digits': '987',
            'card_expiry_date': '01/20',
            'card_brand': 'visa',
            'billing_address': {
                'line1': '102 Petty France',
                'line2': '',
                'postcode': 'SW1H9AJ',
                'city': 'London',
                'country': 'GB',
            },
        }
        govuk_payment = {
            'payment_id': 'payment-id',
            'state': {
                'status': GovUkPaymentStatus.capturable.name,
            },
            'email': '*****@*****.**',
            'provider_id': '123456789',
            'card_details': {
                'cardholder_name': 'John Doe',
                'first_digits_card_number': '1234',
                'last_digits_card_number': '987',
                'expiry_date': '01/20',
                'card_brand': 'visa',
                'billing_address': {
                    'line1': '102 Petty France',
                    'line2': '',
                    'postcode': 'SW1H9AJ',
                    'city': 'London',
                    'country': 'GB',
                },
            },
        }

        with responses.RequestsMock() as rsps:
            mock_auth(rsps)

            # API call related to updating the email address and card details
            rsps.add(
                rsps.PATCH,
                api_url(f'/payments/{payment["uuid"]}/'),
                json={
                    **payment,
                    **payment_extra_details,
                },
                status=200,
            )

            rsps.add(
                rsps.POST,
                govuk_url(f'/payments/{govuk_payment["payment_id"]}/cancel/'),
                status=204,
            )

            status = client.complete_payment_if_necessary(
                payment, govuk_payment)

            payment_patch_body = json.loads(
                rsps.calls[-2].request.body.decode())
            self.assertDictEqual(
                payment_patch_body,
                payment_extra_details,
            )
        self.assertEqual(status, GovUkPaymentStatus.cancelled)
        mock_send_email.assert_not_called()
예제 #7
0
    def test_capturable_payment_that_shouldnt_be_captured_yet(
            self, mock_send_email):
        """
        Test that if the govuk payment is in 'capturable' state, the MTP payment record
        doesn't have the email field filled in and the payment should not be captured yet:

        - the MTP payment record is patched with the card details attributes
        - the method returns GovUkPaymentStatus.capturable
        - an email is sent to the sender
        """
        client = PaymentClient()

        payment = {
            'uuid': 'b74a0eb6-0437-4b22-bce8-e6f11bd43802',
            'recipient_name': 'Alice Re',
            'prisoner_name': 'John Doe',
            'prisoner_number': 'AAB0A00',
            'amount': 1700,
            'security_check': {
                'status': 'pending',
                'user_actioned': False,
            },
        }
        payment_extra_details = {
            'email': '*****@*****.**',
            'worldpay_id': '123456789',
            'cardholder_name': 'John Doe',
            'card_number_first_digits': '1234',
            'card_number_last_digits': '987',
            'card_expiry_date': '01/20',
            'card_brand': 'visa',
            'billing_address': {
                'line1': '102 Petty France',
                'line2': '',
                'postcode': 'SW1H9AJ',
                'city': 'London',
                'country': 'GB',
            },
        }
        govuk_payment = {
            'payment_id': 'payment-id',
            'state': {
                'status': GovUkPaymentStatus.capturable.name,
            },
            'email': '*****@*****.**',
            'provider_id': '123456789',
            'card_details': {
                'cardholder_name': 'John Doe',
                'first_digits_card_number': '1234',
                'last_digits_card_number': '987',
                'expiry_date': '01/20',
                'card_brand': 'visa',
                'billing_address': {
                    'line1': '102 Petty France',
                    'line2': '',
                    'postcode': 'SW1H9AJ',
                    'city': 'London',
                    'country': 'GB',
                },
            },
        }

        with responses.RequestsMock() as rsps:
            mock_auth(rsps)

            # API call related to updating the email address and card details
            rsps.add(
                rsps.PATCH,
                api_url(f'/payments/{payment["uuid"]}/'),
                json={
                    **payment,
                    **payment_extra_details,
                },
                status=200,
            )

            status = client.complete_payment_if_necessary(
                payment, govuk_payment)

            payment_patch_body = json.loads(
                rsps.calls[-1].request.body.decode())
            self.assertDictEqual(
                payment_patch_body,
                payment_extra_details,
            )
        self.assertEqual(status, GovUkPaymentStatus.capturable)
        self.assertEqual(len(mock_send_email.call_args_list), 1)
        send_email_kwargs = mock_send_email.call_args_list[0].kwargs
        self.assertEqual(send_email_kwargs['template_name'],
                         'send-money-debit-card-payment-on-hold')
        self.assertEqual(send_email_kwargs['to'], '*****@*****.**')
예제 #8
0
    def test_success_status(self, mock_send_email):
        """
        Test that if the govuk payment is in 'success' state and the MTP payment record
        doesn't have all the card details and email field filled in:

        - the MTP payment record is patched with the extra payment details
        - the method returns GovUkPaymentStatus.success
        - no email is sent
        """
        client = PaymentClient()

        payment = {
            'uuid': 'some-id',
        }
        payment_extra_details = {
            'email': '*****@*****.**',
            'cardholder_name': 'John Doe',
            'card_number_first_digits': '1234',
            'card_number_last_digits': '987',
            'card_expiry_date': '01/20',
            'card_brand': 'visa',
            'billing_address': {
                'line1': '102 Petty France',
                'line2': '',
                'postcode': 'SW1H9AJ',
                'city': 'London',
                'country': 'GB',
            },
        }
        govuk_payment = {
            'payment_id': 'payment-id',
            'state': {
                'status': GovUkPaymentStatus.success.name,
            },
            'email': '*****@*****.**',
            'card_details': {
                'cardholder_name': 'John Doe',
                'first_digits_card_number': '1234',
                'last_digits_card_number': '987',
                'expiry_date': '01/20',
                'card_brand': 'visa',
                'billing_address': {
                    'line1': '102 Petty France',
                    'line2': '',
                    'postcode': 'SW1H9AJ',
                    'city': 'London',
                    'country': 'GB',
                },
            },
        }
        with responses.RequestsMock() as rsps:
            mock_auth(rsps)

            # API call related to updating the email address and other details on the payment record
            rsps.add(
                rsps.PATCH,
                api_url(f'/payments/{payment["uuid"]}/'),
                json={
                    **payment,
                    **payment_extra_details,
                },
                status=200,
            )

            status = client.complete_payment_if_necessary(
                payment, govuk_payment)

            self.assertDictEqual(
                json.loads(rsps.calls[-1].request.body.decode()),
                payment_extra_details,
            )

        self.assertEqual(status, GovUkPaymentStatus.success)
        mock_send_email.assert_not_called()