def test_with_govuk_pay_erroring_when_refreshing(self, requests_mock):
        """
        Test that if GOV.UK Pay cancels the payment but errors when
        refreshing the session, the session object is not updated
        (but the GOV.UK payment is still cancelled).
        This is okay as the session object will get refreshed at the next
        opportunity.
        """
        session = PaymentGatewaySessionFactory()
        original_session_status = session.status
        requests_mock.post(
            govuk_url(f'payments/{session.govuk_payment_id}/cancel'),
            status_code=204,
        )
        requests_mock.get(
            govuk_url(f'payments/{session.govuk_payment_id}'),
            status_code=500,
        )

        with pytest.raises(GOVUKPayAPIException):
            session.cancel()

        session.refresh_from_db()
        assert session.status == original_session_status

        assert requests_mock.call_count == 2
    def test_create_first_session(self, requests_mock, public_omis_api_client):
        """
        Test a successful call to create a payment gateway session.

        This starts a GOV.UK payment and creates an OMIS payment gateway session
        object tracking it.
        """
        # mock GOV.UK response
        govuk_payment_id = '123abc123abc123abc123abc12'
        next_url = 'https://payment.example.com/123abc'
        json_response = {
            'state': {'status': 'created', 'finished': False},
            'payment_id': govuk_payment_id,
            '_links': {
                'next_url': {
                    'href': next_url,
                    'method': 'GET',
                },
            },
        }
        requests_mock.post(
            govuk_url('payments'),  # create payment
            status_code=201,
            json=json_response,
        )
        requests_mock.get(
            govuk_url(f'payments/{govuk_payment_id}'),  # get payment
            status_code=200,
            json=json_response,
        )

        assert PaymentGatewaySession.objects.count() == 0

        # make API call
        order = OrderWithAcceptedQuoteFactory()
        url = reverse(
            'api-v3:public-omis:payment-gateway-session:collection',
            kwargs={'public_token': order.public_token},
        )
        response = public_omis_api_client.post(url, json_={})
        assert response.status_code == status.HTTP_201_CREATED

        # check payment gateway session record created
        assert PaymentGatewaySession.objects.count() == 1
        session = PaymentGatewaySession.objects.first()
        assert session.govuk_payment_id == govuk_payment_id
        assert session.status == PaymentGatewaySessionStatus.CREATED

        # check API response
        assert response.json() == {
            'id': str(session.id),
            'created_on': format_date_or_datetime(session.created_on),
            'status': PaymentGatewaySessionStatus.CREATED,
            'payment_url': next_url,
        }
    def test_429_if_too_many_requests_made(
        self,
        local_memory_cache,
        requests_mock,
        monkeypatch,
        public_omis_api_client,
    ):
        """Test that the throttling for the create endpoint works if its rate is set."""
        monkeypatch.setitem(
            settings.REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'],
            'payment_gateway_session.create',
            '3/sec',
        )

        # mock GOV.UK response
        govuk_payment_id = '123abc123abc123abc123abc12'
        json_response = {
            'state': {'status': 'created', 'finished': False},
            'payment_id': govuk_payment_id,
            '_links': {
                'next_url': {
                    'href': 'https://payment.example.com/123abc',
                    'method': 'GET',
                },
            },
        }
        requests_mock.post(
            govuk_url('payments'),  # create payment
            status_code=201,
            json=json_response,
        )
        requests_mock.get(
            govuk_url(f'payments/{govuk_payment_id}'),  # get payment
            status_code=200,
            json=json_response,
        )
        requests_mock.post(
            govuk_url(f'payments/{govuk_payment_id}/cancel'),  # cancel payment
            status_code=204,
        )

        order = OrderWithAcceptedQuoteFactory()

        url = reverse(
            'api-v3:public-omis:payment-gateway-session:collection',
            kwargs={'public_token': order.public_token},
        )

        # the 4th time it should error
        for _ in range(3):
            response = public_omis_api_client.post(url, json_={})
            assert response.status_code == status.HTTP_201_CREATED

        response = public_omis_api_client.post(url, json_={})
        assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS
    def test_500_if_govuk_pay_errors_when_cancelling(self, govuk_status_code,
                                                     requests_mock):
        """
        Test that if GOV.UK Pay errors whilst cancelling some other ongoing
        sessions/payments, the endpoint returns 500 to keep the system consistent.

        Possible GOV.UK errors when cancelling:
        - 400 - BAD REQUEST
        - 401 - UNAUTHORIZED
        - 404 - NOT FOUND
        - 409 - CONFLICT
        - 500 - INTERNAL SERVER ERROR

        In all these cases we return 500 as all those GOV.UK errors are unexpected.
        """
        order = OrderWithAcceptedQuoteFactory()
        existing_session = PaymentGatewaySessionFactory(
            order=order,
            status=PaymentGatewaySessionStatus.created,
        )

        # mock GOV.UK requests used to
        # - refresh the existing payment gateway session
        # - cancel the GOV.UK payment
        requests_mock.get(
            govuk_url(f'payments/{existing_session.govuk_payment_id}'),
            status_code=200,
            json={
                'state': {
                    'status': existing_session.status
                },
            },
        )
        requests_mock.post(
            govuk_url(f'payments/{existing_session.govuk_payment_id}/cancel'),
            status_code=govuk_status_code,
        )

        # make API call
        assert PaymentGatewaySession.objects.count() == 1

        url = reverse(
            'api-v3:omis-public:payment-gateway-session:collection',
            kwargs={'public_token': order.public_token},
        )
        client = self.create_api_client(
            scope=Scope.public_omis_front_end,
            grant_type=Application.GRANT_CLIENT_CREDENTIALS,
        )
        response = client.post(url)
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR

        # check no session created
        assert PaymentGatewaySession.objects.count() == 1
    def test_atomicity_when_order_save_errors(self, requests_mock):
        """
        Test that if the order.mark_as_paid() call fails, non of the changes persists.
        """
        session = PaymentGatewaySessionFactory()
        original_session_status = session.status
        url = govuk_url(f'payments/{session.govuk_payment_id}')
        requests_mock.get(
            url,
            status_code=200,
            json={
                'state': {
                    'status': 'success'
                },
            },
        )
        session.order.mark_as_paid = mock.MagicMock(side_effect=Exception())

        with pytest.raises(Exception):
            session.refresh_from_govuk_payment()

        session.refresh_from_db()
        assert session.status == original_session_status

        assert requests_mock.call_count == 1
    def test_with_different_govuk_payment_status_updates_session(
        self,
        govuk_status,
        payment_url,
        requests_mock,
    ):
        """
        Test that if the GOV.UK payment status is not the same as the payment gateway session one,
        the record is updated.
        """
        # choose an initial status != from the govuk one to test the update
        initial_status = PaymentGatewaySessionStatus.created
        if initial_status == govuk_status:
            initial_status = PaymentGatewaySessionStatus.started

        session = PaymentGatewaySessionFactory(status=initial_status)

        # mock GOV.UK call used to get the existing session
        requests_mock.get(
            govuk_url(f'payments/{session.govuk_payment_id}'),
            status_code=200,
            json={
                'state': {
                    'status': govuk_status
                },
                'payment_id': session.govuk_payment_id,
                '_links': {
                    'next_url': None if not payment_url else {
                        'href': payment_url
                    },
                },
            },
        )

        # make API call
        url = reverse(
            'api-v3:omis-public:payment-gateway-session:detail',
            kwargs={
                'public_token': session.order.public_token,
                'pk': session.id
            },
        )
        client = self.create_api_client(
            scope=Scope.public_omis_front_end,
            grant_type=Application.GRANT_CLIENT_CREDENTIALS,
        )
        response = client.get(url)
        assert response.status_code == status.HTTP_200_OK

        # refresh record
        session.refresh_from_db()
        assert session.status == govuk_status

        # check API response
        assert response.json() == {
            'id': str(session.id),
            'created_on': format_date_or_datetime(session.created_on),
            'status': govuk_status,
            'payment_url': payment_url,
        }
Beispiel #7
0
    def test_exception_if_govuk_pay_errors_when_creating(
        self, govuk_status_code, requests_mock,
    ):
        """
        Test that if GOV.UK Pay errors whilst creating a new payment, the method raises
        GOVUKPayAPIException.

        Possible GOV.UK Pay errors:
        - 400 - BAD REQUEST
        - 401 - UNAUTHORIZED
        - 422 - UNPROCESSABLE ENTITY
        - 500 - INTERNAL SERVER ERROR
        """
        requests_mock.post(
            govuk_url('payments'),
            status_code=govuk_status_code,
        )

        assert PaymentGatewaySession.objects.count() == 0

        order = OrderWithAcceptedQuoteFactory()

        with pytest.raises(GOVUKPayAPIException):
            PaymentGatewaySession.objects.create_from_order(order)

        assert PaymentGatewaySession.objects.count() == 0
    def test_with_different_govuk_payment_status_updates_session(
            self, status, requests_mock):
        """
        Test that if the GOV.UK payment status is not the same as the payment gateway session one,
        the record is updated.
        """
        # choose an initial status != from the govuk one to test the update
        initial_status = PaymentGatewaySessionStatus.created
        if initial_status == status:
            initial_status = PaymentGatewaySessionStatus.started

        session = PaymentGatewaySessionFactory(status=initial_status)
        url = govuk_url(f'payments/{session.govuk_payment_id}')
        requests_mock.get(
            url,
            status_code=200,
            json={
                'state': {
                    'status': status
                },
            },
        )

        assert session.refresh_from_govuk_payment()

        session.refresh_from_db()
        assert session.status == status

        assert requests_mock.call_count == 1
    def test_with_value(self, requests_mock):
        """
        Test the value returned when the GOV.UK Pay response data includes next_url.
        """
        govuk_payment_id = '123abc123abc123abc123abc12'
        next_url = 'https://payment.example.com/123abc'
        requests_mock.get(
            govuk_url(f'payments/{govuk_payment_id}'),
            status_code=200,
            json={
                'state': {
                    'status': 'started',
                    'finished': False
                },
                'payment_id': govuk_payment_id,
                '_links': {
                    'next_url': {
                        'href': next_url,
                        'method': 'GET',
                    },
                },
            },
        )

        session = PaymentGatewaySession(govuk_payment_id=govuk_payment_id)
        assert session.get_payment_url() == next_url
    def test_500_if_govuk_pay_errors(self, govuk_status_code, requests_mock):
        """
        Test that if GOV.UK Pay errors whilst getting a payment, the endpoint returns 500.

        Possible GOV.UK errors:
        - 401 - UNAUTHORIZED
        - 404 - NOT FOUND
        - 500 - INTERNAL SERVER ERROR
        """
        order = OrderWithAcceptedQuoteFactory()
        session = PaymentGatewaySessionFactory(
            order=order,
            status=PaymentGatewaySessionStatus.created,
        )

        requests_mock.get(
            govuk_url(f'payments/{session.govuk_payment_id}'),
            status_code=govuk_status_code,
        )

        # make API call
        url = reverse(
            'api-v3:omis-public:payment-gateway-session:detail',
            kwargs={
                'public_token': order.public_token,
                'pk': session.id
            },
        )
        client = self.create_api_client(
            scope=Scope.public_omis_front_end,
            grant_type=Application.GRANT_CLIENT_CREDENTIALS,
        )
        response = client.get(url)
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
    def test_with_unchanged_govuk_payment_status_doesnt_change_anything(
        self,
        status,
        requests_mock,
    ):
        """
        Test that if the GOV.UK payment status is the same as the payment gateway session one,
        (meaning that the payment gateway session is up-to-date), the record is not changed.
        """
        session = PaymentGatewaySession(status=status)
        url = govuk_url(f'payments/{session.govuk_payment_id}')
        requests_mock.get(
            url,
            status_code=200,
            json={
                'state': {
                    'status': status,
                    'finished': False
                },
            },
        )

        assert not session.refresh_from_govuk_payment()
        assert session.status == status
        assert Payment.objects.count() == 0

        assert requests_mock.call_count == 1
Beispiel #12
0
    def test_one_failed_refresh_doesnt_stop_others(self, requests_mock):
        """
        Test that if one refresh fails, the other ones are still carried on
        and committed to the databasea.

        In this example, pay-1 and pay-3 should get refreshed whilst pay-2
        errors and shouldn't get refreshed.
        """
        # mock calls to GOV.UK Pay
        govuk_payment_ids = ['pay-1', 'pay-2', 'pay-3']
        requests_mock.get(
            govuk_url(f'payments/{govuk_payment_ids[0]}'),
            status_code=200,
            json={'state': {
                'status': 'failed'
            }},
        )
        requests_mock.get(
            govuk_url(f'payments/{govuk_payment_ids[1]}'),
            status_code=500,
        )
        requests_mock.get(
            govuk_url(f'payments/{govuk_payment_ids[2]}'),
            status_code=200,
            json={'state': {
                'status': 'failed'
            }},
        )

        # populate db
        sessions = PaymentGatewaySessionFactory.create_batch(
            3,
            status=PaymentGatewaySessionStatus.STARTED,
            govuk_payment_id=factory.Iterator(govuk_payment_ids),
        )

        # make call
        refresh_pending_payment_gateway_sessions(age_check=0)

        # check result
        for session in sessions:
            session.refresh_from_db()

        assert requests_mock.call_count == 3
        assert sessions[0].status == PaymentGatewaySessionStatus.FAILED
        assert sessions[1].status == PaymentGatewaySessionStatus.STARTED
        assert sessions[2].status == PaymentGatewaySessionStatus.FAILED
Beispiel #13
0
    def test_create_first_session_from_order(self, requests_mock, monkeypatch):
        """
        Test the successful creation of the first payment gateway session for an order.
        """
        monkeypatch.setattr(
            'uuid.uuid4',
            mock.Mock(return_value='0123abcd-0000-0000-0000-000000000000'),
        )

        # mock request
        govuk_payment_id = '123abc123abc123abc123abc12'
        govuk_payments_url = govuk_url('payments')
        requests_mock.post(
            govuk_payments_url,
            status_code=201,
            json={
                'state': {'status': 'created', 'finished': False},
                'payment_id': govuk_payment_id,
                '_links': {
                    'next_url': {
                        'href': 'https://payment.example.com/123abc',
                        'method': 'GET',
                    },
                },
            },
        )

        assert PaymentGatewaySession.objects.count() == 0

        # call method
        adviser = AdviserFactory()
        order = OrderWithAcceptedQuoteFactory()
        session = PaymentGatewaySession.objects.create_from_order(
            order=order,
            attrs={'created_by': adviser},
        )

        # check session
        assert session.order == order
        assert session.status == PaymentGatewaySessionStatus.CREATED
        assert session.govuk_payment_id == govuk_payment_id
        assert session.created_by == adviser

        assert PaymentGatewaySession.objects.count() == 1

        # check mocked request
        assert requests_mock.call_count == 1
        assert requests_mock.request_history[-1].url == govuk_payments_url
        assert requests_mock.request_history[-1].json() == {
            'amount': order.total_cost,
            'reference': f'{order.reference}-0123ABCD',
            'description': settings.GOVUK_PAY_PAYMENT_DESCRIPTION.format(
                reference=order.reference,
            ),
            'return_url': settings.GOVUK_PAY_RETURN_URL.format(
                public_token=order.public_token,
                session_id=session.pk,
            ),
        }
Beispiel #14
0
    def test_exception_if_govuk_pay_errors_when_cancelling(
        self, govuk_status_code, requests_mock,
    ):
        """
        Test that if GOV.UK Pay errors whilst cancelling some other ongoing
        sessions/payments, the method raises GOVUKPayAPIException to keep the system consistent.

        Possible GOV.UK Pay errors when cancelling:
        - 400 - BAD REQUEST
        - 401 - UNAUTHORIZED
        - 404 - NOT FOUND
        - 409 - CONFLICT
        - 500 - INTERNAL SERVER ERROR
        """
        order = OrderWithAcceptedQuoteFactory()
        existing_session = PaymentGatewaySessionFactory(
            order=order,
            status=PaymentGatewaySessionStatus.CREATED,
        )

        # mock GOV.UK requests used to
        # - refresh the existing payment gateway session
        # - cancel the GOV.UK payment
        requests_mock.get(
            govuk_url(f'payments/{existing_session.govuk_payment_id}'),
            status_code=200,
            json={
                'state': {'status': existing_session.status},
            },
        )
        requests_mock.post(
            govuk_url(f'payments/{existing_session.govuk_payment_id}/cancel'),
            status_code=govuk_status_code,
        )

        assert PaymentGatewaySession.objects.count() == 1

        with pytest.raises(GOVUKPayAPIException):
            PaymentGatewaySession.objects.create_from_order(order)

        assert PaymentGatewaySession.objects.count() == 1
Beispiel #15
0
    def test_cancel_updates_session(self, requests_mock):
        """
        Test that if GOV.UK Pay cancels and acknowledges the change,
        the session object is updated.
        """
        session = PaymentGatewaySessionFactory()
        requests_mock.post(
            govuk_url(f'payments/{session.govuk_payment_id}/cancel'),
            status_code=204,
        )
        requests_mock.get(
            govuk_url(f'payments/{session.govuk_payment_id}'),
            status_code=200,
            json={'state': {'status': 'cancelled'}},
        )

        session.cancel()

        session.refresh_from_db()
        assert session.status == PaymentGatewaySessionStatus.CANCELLED

        assert requests_mock.call_count == 2
    def test_get(self, order_status, session_status, requests_mock):
        """Test a successful call to get a payment gateway session."""
        order = OrderFactory(status=order_status)
        session = PaymentGatewaySessionFactory(
            order=order,
            status=session_status,
        )

        # mock GOV.UK Pay request used to get the existing session
        next_url = 'https://payment.example.com/123abc'
        requests_mock.get(
            govuk_url(f'payments/{session.govuk_payment_id}'),
            status_code=200,
            json={
                'state': {
                    'status': session.status
                },
                'payment_id': session.govuk_payment_id,
                '_links': {
                    'next_url': {
                        'href': next_url,
                        'method': 'GET',
                    },
                },
            },
        )

        # make API call
        url = reverse(
            'api-v3:omis-public:payment-gateway-session:detail',
            kwargs={
                'public_token': order.public_token,
                'pk': session.id
            },
        )
        client = self.create_api_client(
            scope=Scope.public_omis_front_end,
            grant_type=Application.GRANT_CLIENT_CREDENTIALS,
        )
        response = client.get(url)
        assert response.status_code == status.HTTP_200_OK

        # check API response
        assert response.json() == {
            'id': str(session.id),
            'created_on': format_date_or_datetime(session.created_on),
            'status': session.status,
            'payment_url': next_url,
        }
    def test_atomicity_when_govuk_pay_errors(self, requests_mock):
        """
        Test that if GOV.UK Pay errors, none of the changes persists.
        """
        session = PaymentGatewaySessionFactory()
        original_session_status = session.status

        url = govuk_url(f'payments/{session.govuk_payment_id}')
        requests_mock.get(url, status_code=500)

        with pytest.raises(GOVUKPayAPIException):
            assert session.refresh_from_govuk_payment()

        session.refresh_from_db()
        assert session.status == original_session_status

        assert requests_mock.call_count == 1
Beispiel #18
0
    def test_refresh(self, requests_mock):
        """
        Test that only ongoing sessions older than 60 minutes are refreshed against GOV.UK Pay.
        Note that the value '60 minutes' is a parameter initialised in the test
        and not part of the logic of the task.
        """
        # mock call to GOV.UK Pay
        requests_mock.register_uri(
            'GET',
            re.compile(govuk_url('payments/*')),
            json={'state': {
                'status': 'failed'
            }},
        )

        # populate db
        data = (
            # shouldn't be included because modified_on == 59 mins ago
            ('2017-04-18 19:01', PaymentGatewaySessionStatus.STARTED),

            # shouldn't be included because status != 'ongoing'
            ('2017-04-18 18:59', PaymentGatewaySessionStatus.SUCCESS),
            ('2017-04-18 18:59', PaymentGatewaySessionStatus.FAILED),
            ('2017-04-18 18:59', PaymentGatewaySessionStatus.CANCELLED),
            ('2017-04-18 18:59', PaymentGatewaySessionStatus.ERROR),

            # should be included because modified_on >= 60 mins ago
            ('2017-04-18 19:00', PaymentGatewaySessionStatus.CREATED),
            ('2017-04-18 18:59', PaymentGatewaySessionStatus.STARTED),
            ('2017-04-17 20:00', PaymentGatewaySessionStatus.SUBMITTED),
        )
        sessions = []
        for frozen_time, session_status in data:
            with freeze_time(frozen_time):
                sessions.append(
                    PaymentGatewaySessionFactory(status=session_status))

        # make call
        with freeze_time('2017-04-18 20:00'):  # mocking now
            refresh_pending_payment_gateway_sessions(age_check=60)

        # check result
        assert requests_mock.call_count == 3
        for session in sessions[-3:]:
            session.refresh_from_db()
            assert session.status == PaymentGatewaySessionStatus.FAILED
    def test_with_govuk_pay_erroring_when_cancelling(self, requests_mock):
        """
        Test that if GOV.UK Pay errors when cancelling the payment,
        the session object is not updated.
        """
        session = PaymentGatewaySessionFactory()
        original_session_status = session.status
        requests_mock.post(
            govuk_url(f'payments/{session.govuk_payment_id}/cancel'),
            status_code=500,
        )

        with pytest.raises(GOVUKPayAPIException):
            session.cancel()

        session.refresh_from_db()
        assert session.status == original_session_status

        assert requests_mock.call_count == 1
Beispiel #20
0
    def test_without_value(self, requests_mock):
        """
        Test that an empty string is returned when the GOV.UK Pay response data
        doesn't include next_url.
        """
        govuk_payment_id = '123abc123abc123abc123abc12'
        requests_mock.get(
            govuk_url(f'payments/{govuk_payment_id}'),
            status_code=200,
            json={
                'state': {'status': 'cancelled', 'finished': True},
                'payment_id': govuk_payment_id,
                '_links': {
                    'next_url': None,
                },
            },
        )

        session = PaymentGatewaySession(govuk_payment_id=govuk_payment_id)
        assert session.get_payment_url() == ''
    def test_500_if_govuk_pay_errors_when_creating(self, govuk_status_code,
                                                   requests_mock):
        """
        Test that if GOV.UK Pay errors whilst creating a new payment, the endpoint returns 500.

        Possible GOV.UK errors:
        - 400 - BAD REQUEST
        - 401 - UNAUTHORIZED
        - 422 - UNPROCESSABLE ENTITY
        - 500 - INTERNAL SERVER ERROR

        In all these cases we return 500 as all those GOV.UK errors are unexpected.
        """
        # mock GOV.UK response
        requests_mock.post(
            govuk_url('payments'),
            status_code=govuk_status_code,
        )

        assert PaymentGatewaySession.objects.count() == 0

        # make API call
        order = OrderWithAcceptedQuoteFactory()
        url = reverse(
            'api-v3:omis-public:payment-gateway-session:collection',
            kwargs={'public_token': order.public_token},
        )
        client = self.create_api_client(
            scope=Scope.public_omis_front_end,
            grant_type=Application.GRANT_CLIENT_CREDENTIALS,
        )
        response = client.post(url)
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR

        # check no session created
        assert PaymentGatewaySession.objects.count() == 0
Beispiel #22
0
    def test_exception_if_refresh_updates_order_status_to_paid(self, requests_mock):
        """
        Test that if the system is not up-to-date, the order is in quote_accepted
        but the GOV.UK payment happens, the method triggers a check on existing
        sessions, realises that one finished successfully and records the payment
        marking the order as 'paid'.
        For this reason, the method raises APIConflictException as no other payment can be started.
        """
        # set up db
        order = OrderWithAcceptedQuoteFactory()
        existing_session = PaymentGatewaySessionFactory(
            order=order,
            status=PaymentGatewaySessionStatus.STARTED,
        )

        # mock GOV.UK requests used to refresh the payment session,
        # GOV.UK Pay says that the payment completed successfully
        requests_mock.get(
            govuk_url(f'payments/{existing_session.govuk_payment_id}'),
            status_code=200,
            json={
                'amount': order.total_cost,
                'state': {'status': 'success'},
                'email': '*****@*****.**',
                'created_date': '2018-02-13T14:56:56.734Z',
                'reference': '12345',
                'card_details': {
                    'last_digits_card_number': '1111',
                    'cardholder_name': 'John Doe',
                    'expiry_date': '01/20',
                    'billing_address': {
                        'line1': 'line 1 address',
                        'line2': 'line 2 address',
                        'postcode': 'SW1A 1AA',
                        'city': 'London',
                        'country': 'GB',
                    },
                    'card_brand': 'Visa',
                },
            },
        )

        with pytest.raises(APIConflictException):
            PaymentGatewaySession.objects.create_from_order(order)

        # check session record
        existing_session.refresh_from_db()
        assert existing_session.status == PaymentGatewaySessionStatus.SUCCESS

        # check order and payment
        order.refresh_from_db()
        assert order.status == OrderStatus.PAID

        assert Payment.objects.count() == 1
        payment = Payment.objects.first()

        assert payment.amount == order.total_cost
        assert payment.method == PaymentMethod.CARD
        assert payment.received_on == dateutil_parse('2018-02-13').date()
        assert payment.transaction_reference == '12345'
        assert payment.cardholder_name == 'John Doe'
        assert payment.billing_address_1 == 'line 1 address'
        assert payment.billing_address_2 == 'line 2 address'
        assert payment.billing_address_town == 'London'
        assert payment.billing_address_postcode == 'SW1A 1AA'
        assert payment.billing_address_country == 'GB'
        assert payment.billing_email == '*****@*****.**'
        assert payment.card_brand == 'Visa'
    def test_with_govuk_payment_success_updates_order(self, requests_mock):
        """
        Test that if the GOV.UK payment status is `success` and the payment gateway session is
        out of date, the record is updated, the related order marked as `paid` and an OMIS
        `payment.Payment` record created from the GOV.UK response data one.
        """
        order = OrderWithAcceptedQuoteFactory()
        session = PaymentGatewaySessionFactory(
            status=PaymentGatewaySessionStatus.created,
            order=order,
        )
        url = govuk_url(f'payments/{session.govuk_payment_id}')
        response_json = {
            'amount': order.total_cost,
            'state': {
                'status': 'success'
            },
            'email': '*****@*****.**',
            'created_date': '2018-02-13T14:56:56.734Z',
            'reference': '12345',
            'card_details': {
                'last_digits_card_number': '1111',
                'cardholder_name': 'John Doe',
                'expiry_date': '01/20',
                'billing_address': {
                    'line1': 'line 1 address',
                    'line2': 'line 2 address',
                    'postcode': 'SW1A 1AA',
                    'city': 'London',
                    'country': 'GB',
                },
                'card_brand': 'Visa',
            },
        }
        requests_mock.get(url, status_code=200, json=response_json)

        assert session.refresh_from_govuk_payment()

        # check session
        session.refresh_from_db()
        assert session.status == PaymentGatewaySessionStatus.success

        # check order
        order.refresh_from_db()
        assert order.status == OrderStatus.paid

        # check payment object
        assert Payment.objects.filter(order=order).count() == 1

        payment = Payment.objects.filter(order=order).first()
        assert payment.amount == response_json['amount']
        assert payment.method == PaymentMethod.card
        assert payment.received_on == dateutil_parse('2018-02-13').date()
        assert payment.transaction_reference == '12345'

        assert payment.cardholder_name == 'John Doe'
        assert payment.card_brand == 'Visa'
        assert payment.billing_email == '*****@*****.**'
        assert payment.billing_address_1 == 'line 1 address'
        assert payment.billing_address_2 == 'line 2 address'
        assert payment.billing_address_town == 'London'
        assert payment.billing_address_postcode == 'SW1A 1AA'
        assert payment.billing_address_country == 'GB'

        assert requests_mock.call_count == 1
    def test_create_cancels_other_ongoing_sessions(self, requests_mock):
        """
        Test that creating a new payment gateway session cancels
        the other ongoing sessions and GOV.UK payments.

        Given:
            - ongoing session 1
            - ongoing session 2
            - failed session 3

        Calling this endpoint should:
            - cancel GOV.UK payment related to session 1
            - update the payment gateway session 1 status to 'cancelled'

            - cancel GOV.UK payment related to session 2
            - update the payment gateway session 2 status to 'cancelled'

            - start a new GOV.UK payment
            - create a payment gateway session related to it
        """
        order = OrderWithAcceptedQuoteFactory()
        existing_data = PaymentGatewaySessionFactory.create_batch(
            3,
            order=order,
            status=factory.Iterator([
                PaymentGatewaySessionStatus.created,
                PaymentGatewaySessionStatus.started,
                PaymentGatewaySessionStatus.failed,
            ]),
        )

        # mock GOV.UK requests used to:
        # - refresh the payment gateway sessions
        # - cancel the GOV.UK payments
        # - refresh the payment gateway sessions again after the cancellation
        for session in existing_data:
            requests_mock.get(
                govuk_url(f'payments/{session.govuk_payment_id}'),
                [
                    # this is for the initial refresh
                    {
                        'status_code': 200,
                        'json': {
                            'state': {
                                'status': session.status
                            }
                        },
                    },
                    # this is for the second refresh after cancelling
                    {
                        'status_code': 200,
                        'json': {
                            'state': {
                                'status': 'cancelled'
                            }
                        },
                    },
                ],
            )
            requests_mock.post(
                govuk_url(f'payments/{session.govuk_payment_id}/cancel'),
                status_code=204,
            )

        # mock GOV.UK call used to create a new payment session
        govuk_payment_id = '123abc123abc123abc123abc12'
        next_url = 'https://payment.example.com/123abc'
        json_response = {
            'state': {
                'status': 'created',
                'finished': False
            },
            'payment_id': govuk_payment_id,
            '_links': {
                'next_url': {
                    'href': next_url,
                    'method': 'GET',
                },
            },
        }
        requests_mock.post(
            govuk_url('payments'),  # create payment
            status_code=201,
            json=json_response,
        )
        requests_mock.get(
            govuk_url(f'payments/{govuk_payment_id}'),  # get payment
            status_code=200,
            json=json_response,
        )

        # make API call
        url = reverse(
            'api-v3:omis-public:payment-gateway-session:collection',
            kwargs={'public_token': order.public_token},
        )
        client = self.create_api_client(
            scope=Scope.public_omis_front_end,
            grant_type=Application.GRANT_CLIENT_CREDENTIALS,
        )
        response = client.post(url)
        assert response.status_code == status.HTTP_201_CREATED

        # check sessions cancelled
        for existing_session in existing_data[:-1]:
            existing_session.refresh_from_db()
            assert existing_session.status == PaymentGatewaySessionStatus.cancelled

        # check session record created
        assert PaymentGatewaySession.objects.ongoing().count() == 1
        session = PaymentGatewaySession.objects.ongoing().first()
        assert session.govuk_payment_id == govuk_payment_id

        # check API response
        assert response.json() == {
            'id': str(session.id),
            'created_on': format_date_or_datetime(session.created_on),
            'status': PaymentGatewaySessionStatus.created,
            'payment_url': next_url,
        }
Beispiel #25
0
    def test_create_cancels_other_sessions(self, requests_mock):
        """
        Test that creating a new payment gateway session cancels
        the other ongoing sessions and GOV.UK payments.

        Given:
            - ongoing session 1
            - ongoing session 2
            - failed session 3

        Calling .create_from_order should:
            - cancel the GOV.UK payment related to session 1
            - update the payment gateway session 1 status to 'cancelled'

            - cancel the GOV.UK payment related to session 2
            - update the payment gateway session 2 status to 'cancelled'

            - start a new GOV.UK payment
            - create a payment gateway session related to it
        """
        order = OrderWithAcceptedQuoteFactory()
        existing_data = PaymentGatewaySessionFactory.create_batch(
            3,
            order=order,
            status=factory.Iterator([
                PaymentGatewaySessionStatus.CREATED,
                PaymentGatewaySessionStatus.STARTED,
                PaymentGatewaySessionStatus.FAILED,
            ]),
        )

        # mock GOV.UK requests used to:
        # - refresh the payment gateway sessions
        # - cancel the GOV.UK payments
        # - refresh the payment gateway sessions again after the cancellation
        for session in existing_data:
            requests_mock.get(
                govuk_url(f'payments/{session.govuk_payment_id}'),
                [
                    # this is for the initial refresh
                    {
                        'status_code': 200,
                        'json': {'state': {'status': session.status}},
                    },
                    # this is for the second refresh after cancelling
                    {
                        'status_code': 200,
                        'json': {'state': {'status': 'cancelled'}},
                    },
                ],
            )
            requests_mock.post(
                govuk_url(f'payments/{session.govuk_payment_id}/cancel'),
                status_code=204,
            )

        # mock GOV.UK request used to create a new payment session
        govuk_payment_id = '123abc123abc123abc123abc12'
        requests_mock.post(
            govuk_url('payments'),
            status_code=201,
            json={
                'state': {'status': 'created', 'finished': False},
                'payment_id': govuk_payment_id,
                '_links': {
                    'next_url': {
                        'href': 'https://payment.example.com/123abc',
                        'method': 'GET',
                    },
                },
            },
        )

        assert PaymentGatewaySession.objects.count() == 3

        session = PaymentGatewaySession.objects.create_from_order(order=order)

        # check sessions cancelled
        for existing_session in existing_data[:-1]:
            existing_session.refresh_from_db()
            assert existing_session.status == PaymentGatewaySessionStatus.CANCELLED

        assert PaymentGatewaySession.objects.count() == 4

        # check session record created
        session.refresh_from_db()
        assert session.govuk_payment_id == govuk_payment_id

        # check mocked requests:
        #   2 refresh / 2 cancel - 2 refresh / 1 create
        assert requests_mock.call_count == (2 + 2 + 2 + 1)
        assert requests_mock.request_history[-1].json() == {
            'amount': order.total_cost,
            'reference': f'{order.reference}-{str(session.id)[:8].upper()}',
            'description': settings.GOVUK_PAY_PAYMENT_DESCRIPTION.format(
                reference=order.reference,
            ),
            'return_url': settings.GOVUK_PAY_RETURN_URL.format(
                public_token=order.public_token,
                session_id=session.id,
            ),
        }
    def test_with_govuk_payment_success_updates_order(self, requests_mock):
        """
        Test that if the GOV.UK payment status is `success` and the payment gateway session is
        out of date, the record is updated, the related order marked as `paid` and an OMIS
        `payment.Payment` record created from the GOV.UK response data one.
        """
        order = OrderWithAcceptedQuoteFactory()
        session = PaymentGatewaySessionFactory(
            order=order,
            status=PaymentGatewaySessionStatus.created,
        )

        # mock GOV.UK calls used to refresh the payment session
        # Pay says that the payment completed successfully
        requests_mock.get(
            govuk_url(f'payments/{session.govuk_payment_id}'),
            status_code=200,
            json={
                'amount': order.total_cost,
                'state': {
                    'status': 'success'
                },
                'email': '*****@*****.**',
                'reference': '12345',
                'created_date': '2018-02-13T14:56:56.734Z',
                '_links': {
                    'next_url': None,
                },
                'card_details': {
                    'last_digits_card_number': '1111',
                    'cardholder_name': 'John Doe',
                    'expiry_date': '01/20',
                    'billing_address': {
                        'line1': 'line 1 address',
                        'line2': 'line 2 address',
                        'postcode': 'SW1A 1AA',
                        'city': 'London',
                        'country': 'GB',
                    },
                    'card_brand': 'Visa',
                },
            },
        )

        # make API call
        url = reverse(
            'api-v3:omis-public:payment-gateway-session:detail',
            kwargs={
                'public_token': order.public_token,
                'pk': session.id
            },
        )
        client = self.create_api_client(
            scope=Scope.public_omis_front_end,
            grant_type=Application.GRANT_CLIENT_CREDENTIALS,
        )
        response = client.get(url)
        assert response.status_code == status.HTTP_200_OK

        # check API response
        assert response.json() == {
            'id': str(session.id),
            'created_on': format_date_or_datetime(session.created_on),
            'status': PaymentGatewaySessionStatus.success,
            'payment_url': '',
        }

        # check session record
        session.refresh_from_db()
        assert session.status == PaymentGatewaySessionStatus.success

        # check order and payment
        order.refresh_from_db()
        assert order.status == OrderStatus.paid
        assert Payment.objects.filter(order=order).count() == 1
    def test_409_if_refresh_updates_order_status_to_paid(self, requests_mock):
        """
        Test that if the system is not up-to-date, the order is in quote_accepted
        but the GOV.UK payment happens, the endpoint triggers a check on existing
        sessions, realises that one finished successfully and records the payment
        marking the order as 'paid'.
        For this reason, the endpoint returns 409 - Conflict as no other payment
        can be made.
        """
        # set up db
        order = OrderWithAcceptedQuoteFactory()
        existing_session = PaymentGatewaySessionFactory(
            order=order,
            status=PaymentGatewaySessionStatus.started,
        )

        # mock GOV.UK requests used to refresh the payment session.
        # GOV.UK Pay says that the payment completed successfully
        requests_mock.get(
            govuk_url(f'payments/{existing_session.govuk_payment_id}'),
            status_code=200,
            json={
                'amount': order.total_cost,
                'state': {
                    'status': 'success'
                },
                'email': '*****@*****.**',
                'reference': '12345',
                'created_date': '2018-02-13T14:56:56.734Z',
                'card_details': {
                    'last_digits_card_number': '1111',
                    'cardholder_name': 'John Doe',
                    'expiry_date': '01/20',
                    'billing_address': {
                        'line1': 'line 1 address',
                        'line2': 'line 2 address',
                        'postcode': 'SW1A 1AA',
                        'city': 'London',
                        'country': 'GB',
                    },
                    'card_brand': 'Visa',
                },
            },
        )

        # make API call
        url = reverse(
            'api-v3:omis-public:payment-gateway-session:collection',
            kwargs={'public_token': order.public_token},
        )
        client = self.create_api_client(
            scope=Scope.public_omis_front_end,
            grant_type=Application.GRANT_CLIENT_CREDENTIALS,
        )
        response = client.post(url)
        assert response.status_code == status.HTTP_409_CONFLICT

        # check session record
        existing_session.refresh_from_db()
        assert existing_session.status == PaymentGatewaySessionStatus.success

        # check order and pyament
        order.refresh_from_db()
        assert order.status == OrderStatus.paid

        assert Payment.objects.filter(order=order).count() == 1