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 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_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 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()