def test_with_none_payment(self): """ Test that it returns the non-falsy attrs in govuk_payment if the passed-in payment is falsy. """ client = PaymentClient() payment = None govuk_payment = { 'email': '*****@*****.**', 'provider_id': '', 'card_details': { 'cardholder_name': None, 'card_brand': 'visa', }, 'extra_attribute': 'some-value', } attr_updates = client.get_completion_payment_attr_updates(payment, govuk_payment) self.assertEqual( attr_updates, { 'email': '*****@*****.**', 'card_brand': 'visa', } )
def test_conflict(self, mock_send_email): """ Test that if GOV.UK Pay returns 409 when cancelling a payment, the method raises an HTTPError. """ client = PaymentClient() payment_id = 'invalid' govuk_payment = { 'payment_id': payment_id, 'state': { 'status': GovUkPaymentStatus.capturable.name, }, } with responses.RequestsMock() as rsps: rsps.add( rsps.POST, govuk_url(f'/payments/{payment_id}/cancel/'), status=409, ) with self.assertRaises(HTTPError) as e: client.cancel_govuk_payment(govuk_payment) self.assertEqual( e.exception.response.status_code, 409, ) mock_send_email.assert_not_called()
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()
def get(self, request): prisoner_details = self.valid_form_data[ DebitCardPrisonerDetailsView.url_name] amount_details = self.valid_form_data[DebitCardAmountView.url_name] amount_pence = int(amount_details['amount'] * 100) service_charge_pence = int( get_service_charge(amount_details['amount']) * 100) user_ip = request.META.get('HTTP_X_FORWARDED_FOR', '') user_ip = user_ip.split(',')[0].strip() or None payment_ref = None failure_context = {'short_payment_ref': _('Not known')} try: payment_client = PaymentClient() new_payment = { 'amount': amount_pence, 'service_charge': service_charge_pence, 'recipient_name': prisoner_details['prisoner_name'], 'prisoner_number': prisoner_details['prisoner_number'], 'prisoner_dob': prisoner_details['prisoner_dob'].isoformat(), 'ip_address': user_ip, } payment_ref = payment_client.create_payment(new_payment) failure_context['short_payment_ref'] = payment_ref[:8] new_govuk_payment = { 'delayed_capture': should_be_capture_delayed(), 'amount': amount_pence + service_charge_pence, 'reference': payment_ref, 'description': gettext('To this prisoner: %(prisoner_number)s' % prisoner_details), 'return_url': site_url( build_view_url(self.request, DebitCardConfirmationView.url_name)) + '?payment_ref=' + payment_ref, } if new_govuk_payment['delayed_capture']: logger.info('Starting delayed capture for %(payment_ref)s', {'payment_ref': payment_ref}) govuk_payment = payment_client.create_govuk_payment( payment_ref, new_govuk_payment) if govuk_payment: return redirect(get_link_by_rel(govuk_payment, 'next_url')) except OAuth2Error: logger.exception('Authentication error') except RequestException: logger.exception('Failed to create new payment (ref %s)', payment_ref) return render(request, 'send_money/debit-card-error.html', failure_context)
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 doesn't do anything. """ client = PaymentClient() govuk_payment = {} returned_status = client.cancel_govuk_payment(govuk_payment) self.assertEqual(returned_status, None) mock_send_email.assert_not_called()
def test_do_nothing_if_govukpayment_is_falsy(self): """ Test that if the passed in govuk payment dict is falsy, the method doesn't do anything. """ client = PaymentClient() govuk_payment = {} returned_status = client.cancel_govuk_payment(govuk_payment) self.assertEqual(returned_status, None) self.assertEqual(len(mail.outbox), 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()
def test_get(self): """ Test that the completion values in govuk_payment that are not already set in payment are returned. """ client = PaymentClient() payment = { 'email': '*****@*****.**', # shouldn't get overridden 'worldpay_id': '', # should get updated 'cardholder_name': None, # should get updated 'card_brand': 'visa', # hasn't changed so should be ignored } govuk_payment = { 'email': '*****@*****.**', # should be ignored 'provider_id': '123456789', # should be used 'card_details': { 'cardholder_name': 'John Doe', 'first_digits_card_number': '1234', 'last_digits_card_number': '987', 'expiry_date': '01/20', 'card_brand': 'visa', # hasn't changed so should be ignored 'billing_address': { 'line1': '102 Petty France', 'line2': '', 'postcode': 'SW1H9AJ', 'city': 'London', 'country': 'GB', }, }, 'extra_attribute': 'some-value', } attr_updates = client.get_completion_payment_attr_updates(payment, govuk_payment) self.assertEqual( attr_updates, { 'worldpay_id': '123456789', 'cardholder_name': 'John Doe', 'card_number_first_digits': '1234', 'card_number_last_digits': '987', 'card_expiry_date': '01/20', 'billing_address': { 'line1': '102 Petty France', 'line2': '', 'postcode': 'SW1H9AJ', 'city': 'London', 'country': 'GB', }, } )
def test_with_none_govuk_payment(self): """ Test that it returns {} if the passed in govuk payment is falsy. """ client = PaymentClient() payment = { 'worldpay_id': '123456789', 'card_brand': 'visa', } govuk_payment = None attr_updates = client.get_completion_payment_attr_updates(payment, govuk_payment) self.assertEqual(attr_updates, {})
def test_500(self): """ Test that if GOV.UK Pay returns 500, the method raises HTTPError. """ payment_id = 'payment-id' client = PaymentClient() with responses.RequestsMock() as rsps: rsps.add( rsps.GET, govuk_url(f'/payments/{payment_id}/events/'), status=500, ) with self.assertRaises(HTTPError): client.get_govuk_payment_events(payment_id)
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()
def test_invalid_response(self): """ Test that if the GOV.UK Pay response doesn't have the expected structure, the method raises RequestException. """ payment_id = 'payment-id' client = PaymentClient() with responses.RequestsMock() as rsps: rsps.add(rsps.GET, govuk_url(f'/payments/{payment_id}/events/'), status=200, json={ 'unexpected-key': 'unexpected-value', }) with self.assertRaises(RequestException): client.get_govuk_payment_events(payment_id)
def test_successful(self): """ Test that the method returns events information about a certain govuk payment. """ payment_id = 'payment-id' expected_events = [ { 'payment_id': payment_id, 'state': { 'status': 'created', 'finished': True, 'message': 'User cancelled the payment', 'code': 'P010', }, 'updated': '2017-01-10T16:44:48.646Z', '_links': { 'payment_url': { 'href': 'https://an.example.link/from/payment/platform', 'method': 'GET', }, }, }, ] client = PaymentClient() with responses.RequestsMock() as rsps: rsps.add( rsps.GET, govuk_url(f'/payments/{payment_id}/events/'), status=200, json={ 'events': expected_events, 'payment_id': payment_id, '_links': { 'self': { 'hrefTrue': 'https://an.example.link/from/payment/platform', 'method': 'GET', }, }, }) actual_events = client.get_govuk_payment_events(payment_id) self.assertListEqual(actual_events, expected_events)
def get(self, request, *args, **kwargs): payment_ref = self.request.GET.get('payment_ref') try: # check payment status payment_client = PaymentClient() payment = payment_client.get_payment(payment_ref) if not payment or payment['status'] != 'pending': # bail out if accessed without specifying a payment in pending state return clear_session_view(request) kwargs.update({ 'short_payment_ref': payment_ref[:8].upper(), 'prisoner_name': payment['recipient_name'], 'amount': decimal.Decimal(payment['amount']) / 100, 'email_sent': False, }) # check gov.uk payment status govuk_id = payment['processor_id'] self.success, kwargs = payment_client.check_govuk_payment_status( payment_ref, govuk_id, kwargs ) if not self.success: return redirect(self.build_view_url(DebitCardCheckView.url_name)) except OAuth2Error: logger.exception('Authentication error while processing %s' % payment_ref) except SlumberHttpBaseException as error: error_message = 'Error while processing %s' % payment_ref if hasattr(error, 'content'): error_message += '\nReceived: %s' % error.content logger.exception(error_message) except RequestsTimeout: logger.exception('GOV.UK Pay payment check timed out for %s' % payment_ref) except RequestException as error: error_message = 'GOV.UK Pay payment check failed for %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: logger.exception('GOV.UK Pay payment status incomplete for %s' % payment_ref) response = super().get(request, *args, **kwargs) request.session.flush() return response
def get(self, request): prisoner_details = self.valid_form_data[DebitCardPrisonerDetailsView.url_name] amount_details = self.valid_form_data[DebitCardAmountView.url_name] amount_pence = int(amount_details['amount'] * 100) service_charge_pence = int(get_service_charge(amount_details['amount']) * 100) payment_ref = None failure_context = { 'short_payment_ref': _('Not known') } try: payment_client = PaymentClient() new_payment = { 'amount': amount_pence, 'service_charge': service_charge_pence, 'recipient_name': prisoner_details['prisoner_name'], 'prisoner_number': prisoner_details['prisoner_number'], 'prisoner_dob': prisoner_details['prisoner_dob'].isoformat(), } payment_ref = payment_client.create_payment(new_payment) failure_context['short_payment_ref'] = payment_ref[:8] new_govuk_payment = { 'amount': amount_pence + service_charge_pence, 'reference': payment_ref, 'description': gettext('To this prisoner: %(prisoner_number)s' % prisoner_details), 'return_url': site_url( self.build_view_url(DebitCardConfirmationView.url_name) + '?payment_ref=' + payment_ref ), } govuk_payment = payment_client.create_govuk_payment(payment_ref, new_govuk_payment) if govuk_payment: return redirect(get_link_by_rel(govuk_payment, 'next_url')) except OAuth2Error: logger.exception('Authentication error') except SlumberHttpBaseException: logger.exception('Failed to create new payment') except RequestsTimeout: logger.exception( 'GOV.UK Pay payment initiation timed out for %s' % payment_ref ) return render(request, 'send_money/debit-card-failure.html', failure_context)
def test_do_nothing_if_payment_in_finished_state(self, mock_send_email): """ Test that if the govuk payment is already in a finished state, the method doesn't do anything. """ finished_statuses = [ status for status in GovUkPaymentStatus if status.finished() ] for status in finished_statuses: govuk_payment = { 'payment_id': 'payment-id', 'state': { 'status': status.name, }, } client = PaymentClient() returned_status = client.cancel_govuk_payment(govuk_payment) self.assertEqual(returned_status, status) mock_send_email.assert_not_called()
def test_capture(self): """ Test that if the govuk payment is in 'capturable' state, the method captures the payment and no email is sent. If the method is called again, nothing happen so that to avoid side effects. """ client = PaymentClient() payment_id = 'payment-id' govuk_payment = { 'payment_id': payment_id, 'state': { 'status': GovUkPaymentStatus.capturable.name, }, 'email': '*****@*****.**', } with responses.RequestsMock() as rsps: rsps.add( rsps.POST, govuk_url(f'/payments/{payment_id}/capture/'), status=204, ) returned_status = client.capture_govuk_payment(govuk_payment) self.assertEqual(returned_status, GovUkPaymentStatus.success) self.assertEqual( govuk_payment['state']['status'], GovUkPaymentStatus.success.name, ) self.assertEqual(len(mail.outbox), 0) # try to capture the payment again, nothing should happen client.capture_govuk_payment(govuk_payment) self.assertEqual(len(mail.outbox), 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
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()
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'], '*****@*****.**')
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()