def _test_captured_payment_doesnt_get_updated_before_capture( self, settlement_summary, mock_send_email): govuk_payment_data = { 'payment_id': PAYMENT_DATA['processor_id'], 'reference': PAYMENT_DATA['uuid'], 'state': { 'status': 'capturable' }, 'email': '*****@*****.**', } with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': 1, 'results': [ { **PAYMENT_DATA, 'security_check': { 'status': 'accepted', 'user_actioned': True, }, }, ], }, status=200, ) rsps.add( rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), json=govuk_payment_data, status=200, ) rsps.add( rsps.POST, govuk_url( f'/payments/{PAYMENT_DATA["processor_id"]}/capture/'), status=204, ) rsps.add( rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), json={ **govuk_payment_data, 'state': { 'status': 'success' }, 'settlement_summary': settlement_summary, }, status=200, ) call_command('update_incomplete_payments', verbosity=0) mock_send_email.assert_not_called()
def test_failure_page(self): processor_id = '3' with responses.RequestsMock() as rsps: rsps.add(rsps.GET, api_url('/payments/'), json={ 'uuid': 'wargle-blargle', 'processor_id': processor_id, 'recipient_name': 'James Bond', 'amount': 2000, 'status': 'pending', 'created': datetime.datetime.now().isoformat() + 'Z', 'prisoner_number': 'A5544CD', 'prisoner_dob': '1992-12-05', }) rsps.add(rsps.GET, govuk_url('/payments/%s' % processor_id), json={'state': { 'status': 'failed' }}) self.driver.get( self.live_server_url + '/en-gb/debit-card/confirmation/?payment_ref=REF12345') self.assertInSource( 'We’re sorry, your payment could not be processed on this occasion' ) self.assertInSource( 'Your reference number is <strong>REF12345</strong>')
def test_update_incomplete_payments_no_govuk_payment_found(self): """ Test that if GOV.UK Pay returns 404 for one payment, the command marks the related MTP payment as failed. """ with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': 1, 'results': [PAYMENT_DATA], }, status=200, ) rsps.add( rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), status=404, ) rsps.add( rsps.PATCH, api_url(f'/payments/{PAYMENT_DATA["uuid"]}/'), json={ **PAYMENT_DATA, 'status': 'failed', }, status=200, ) call_command('update_incomplete_payments', verbosity=0) self.assertEqual(rsps.calls[3].request.body.decode(), '{"status": "failed"}')
def _test_update_incomplete_payments_doesnt_update_before_capture(self, settlement_summary): with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': 1, 'results': [PAYMENT_DATA], }, status=200, ) rsps.add( rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), json={ 'reference': PAYMENT_DATA['uuid'], 'state': {'status': 'success'}, 'settlement_summary': settlement_summary, 'email': '*****@*****.**', }, status=200, ) call_command('update_incomplete_payments', verbosity=0) self.assertEqual(len(mail.outbox), 0)
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_debit_card_payment(self): self.choose_debit_card_payment_method() self.fill_in_prisoner_details() self.fill_in_amount() with responses.RequestsMock() as rsps: ref = 'wargle-blargle' processor_id = '3' mock_auth(rsps) rsps.add( rsps.POST, api_url('/payments/'), json={'uuid': ref}, status=201, ) rsps.add( rsps.POST, govuk_url('/payments/'), json={ 'payment_id': processor_id, '_links': { 'next_url': { 'method': 'GET', 'href': govuk_url(self.payment_process_path), } } }, status=201 ) rsps.add( rsps.PATCH, api_url('/payments/%s/' % ref), status=200, ) with self.patch_prisoner_details_check(): response = self.client.get(self.url, follow=False) # check amount and service charge submitted to api self.assertEqual(json.loads(rsps.calls[1].request.body)['amount'], 1700) self.assertEqual(json.loads(rsps.calls[1].request.body)['service_charge'], 61) # check total charge submitted to govuk self.assertEqual(json.loads(rsps.calls[2].request.body)['amount'], 1761) self.assertRedirects( response, govuk_url(self.payment_process_path), fetch_redirect_response=False )
def _test_received_at_date_matches_captured_date( self, capture_submit_time, captured_date, received_at, mock_send_email, ): with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': 1, 'results': [PAYMENT_DATA], }, status=200, ) rsps.add(rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), json={ 'reference': PAYMENT_DATA['uuid'], 'state': { 'status': 'success' }, 'settlement_summary': { 'capture_submit_time': capture_submit_time, 'captured_date': captured_date }, 'email': '*****@*****.**', }, status=200) rsps.add( rsps.PATCH, api_url(f'/payments/{PAYMENT_DATA["uuid"]}/'), json={ **PAYMENT_DATA, 'email': '*****@*****.**', }, status=200, ) call_command('update_incomplete_payments', verbosity=0) self.assertEqual( json.loads( rsps.calls[-1].request.body.decode())['received_at'], received_at) self.assertEqual(len(mock_send_email.call_args_list), 1)
def test_failure_page(self, mocked_client): processor_id = "3" mocked_client().payments().get.return_value = { "processor_id": processor_id, "recipient_name": "James Bond", "amount": 2000, "status": "pending", "created": datetime.datetime.now().isoformat() + "Z", } with responses.RequestsMock() as rsps: rsps.add(rsps.GET, govuk_url("/payments/%s" % processor_id), json={"state": {"status": "failed"}}) self.driver.get(self.live_server_url + "/confirmation/?payment_ref=REF12345") self.assertInSource("We’re sorry, your payment could not be processed on this occasion") self.assertInSource("Your reference number is <strong>REF12345</strong>")
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 create_govuk_payment(self, payment_ref, new_govuk_payment): govuk_response = requests.post(govuk_url('/payments'), headers=govuk_headers(), json=new_govuk_payment, timeout=15) try: if govuk_response.status_code != 201: raise ValueError('Status code not 201') govuk_data = govuk_response.json() payment_update = {'processor_id': govuk_data['payment_id']} self.update_payment(payment_ref, payment_update) return govuk_data except (KeyError, ValueError): logger.exception( 'Failed to create new GOV.UK payment for MTP payment %s. Received: %s' % (payment_ref, govuk_response.content))
def get_govuk_payment(self, govuk_id): response = requests.get( govuk_url('/payments/%s' % govuk_id), headers=govuk_headers(), timeout=15 ) if response.status_code != 200: raise RequestException('Status code not 200', response=response) try: data = response.json() try: validate_email(data.get('email')) except ValidationError: data['email'] = None return data except (ValueError, KeyError): raise RequestException('Cannot parse response', response=response)
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 test_confirmation_handles_rejected_card(self): self.choose_debit_card_payment_method() self.fill_in_prisoner_details() self.fill_in_amount() with responses.RequestsMock() as rsps: ref = 'wargle-blargle' processor_id = '3' mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/%s/' % ref), json={ 'processor_id': processor_id, 'recipient_name': 'John', 'amount': 1700, 'status': 'pending', 'created': datetime.datetime.now().isoformat() + 'Z', }, status=200, ) rsps.add( rsps.GET, govuk_url('/payments/%s/' % processor_id), json={ 'state': {'status': 'failed'}, 'email': '*****@*****.**', }, status=200 ) rsps.add( rsps.PATCH, api_url('/payments/%s/' % ref), status=200, ) with self.patch_prisoner_details_check(), self.silence_logger(): response = self.client.get( self.url, {'payment_ref': ref}, follow=True ) self.assertOnPage(response, 'check_details') # check session is kept for key in self.complete_session_keys: self.assertIn(key, self.client.session)
def create_govuk_payment(self, payment_ref, new_govuk_payment): govuk_response = requests.post( govuk_url('/payments'), headers=govuk_headers(), json=new_govuk_payment, timeout=15 ) try: if govuk_response.status_code != 201: raise ValueError('Status code not 201') govuk_data = govuk_response.json() payment_update = { 'processor_id': govuk_data['payment_id'] } self.update_payment(payment_ref, payment_update) return govuk_data except (KeyError, ValueError): logger.exception( 'Failed to create new GOV.UK payment for MTP payment %s. Received: %s' % (payment_ref, govuk_response.content) )
def get_govuk_payment(self, govuk_id): response = requests.get(govuk_url('/payments/%s' % govuk_id), headers=govuk_headers(), timeout=15) if response.status_code != 200: if response.status_code == 404: return None raise RequestException('Unexpected status code: %s' % response.status_code, response=response) try: data = response.json() try: validate_email(data.get('email')) except ValidationError: data['email'] = None return data except (ValueError, KeyError): raise RequestException('Cannot parse response', response=response)
def get_govuk_payment_events(self, govuk_id): """ :return: list with events information about a certain govuk payment. :param govuk_id: id of the govuk payment :raise HTTPError: if GOV.UK Pay returns a 4xx or 5xx response :raise RequestException: if the response body cannot be parsed """ response = requests.get( govuk_url(f'/payments/{govuk_id}/events'), headers=govuk_headers(), timeout=15, ) response.raise_for_status() try: data = response.json() return data['events'] except (ValueError, KeyError): raise RequestException('Cannot parse response', response=response)
def test_debit_card_payment_handles_govuk_errors(self): self.choose_debit_card_payment_method() self.fill_in_prisoner_details() self.fill_in_amount() with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.POST, api_url('/payments/'), json={'uuid': 'wargle-blargle'}, status=201, ) rsps.add( rsps.POST, govuk_url('/payments/'), status=500 ) with self.patch_prisoner_details_check(), self.silence_logger(): response = self.client.get(self.url, follow=False) self.assertContains(response, 'We’re sorry, your payment could not be processed on this occasion')
def test_payment_did_time_out_but_before_capturable(self): """ Test that if the govuk payment failed because of timeout but the payment was not in capturable status at some point in the past, the method returns False. """ payment_id = 'payment-id' govuk_payment = { 'payment_id': payment_id, 'state': { 'status': 'failed', 'code': 'P0020', 'message': 'Payment expired', 'finished': True, }, } with responses.RequestsMock() as rsps: rsps.add( rsps.GET, govuk_url(f'/payments/{payment_id}/events/'), status=200, json={ 'events': [ { 'payment_id': payment_id, 'state': { 'status': 'submitted', 'finished': False, }, }, ], 'payment_id': payment_id, }, ) self.assertFalse( GovUkPaymentStatus.payment_timed_out_after_capturable( govuk_payment), )
def test_update_incomplete_payments_doesnt_sent_email_if_no_captured_date( self, mock_send_email): """ Test that if the MTP payment is in 'pending' and the GOV.UK payment is in 'success' but no captured_date is found in the response, the MTP payment is not marked as successful yet and no email is sent. This is because the actual capture in Worldpay happens at a later time and can fail. The only way to find out if a payment really went through is by checking the captured date, when it becomes available. """ with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': 1, 'results': [PAYMENT_DATA], }, status=200, ) rsps.add( rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), json={ 'reference': PAYMENT_DATA['uuid'], 'state': { 'status': 'success' }, 'email': '*****@*****.**', }, status=200, ) # no call to mark the payment as successful call_command('update_incomplete_payments', verbosity=0) mock_send_email.assert_not_called()
def test_success_page(self, mocked_client): processor_id = "3" mocked_client().payments().get.return_value = { "processor_id": processor_id, "recipient_name": "James Bond", "amount": 2000, "status": "pending", "created": datetime.datetime.now().isoformat() + "Z", } with responses.RequestsMock() as rsps, self.patch_view_chain_form_checking(): rsps.add( rsps.GET, govuk_url("/payments/%s" % processor_id), json={"state": {"status": "success"}, "email": "*****@*****.**"}, ) self.driver.get(self.live_server_url + "/debit-card/confirmation/?payment_ref=REF12345") self.assertInSource("Payment successful") self.assertInSource("<strong>REF12345</strong>") self.assertInSource("James Bond") self.assertInSource("£20") self.assertInSource("Print this page")
def cancel_govuk_payment(self, govuk_payment): """ Cancels a payment in status 'capturable'. :raise HTTPError: if GOV.UK Pay returns a 4xx or 5xx response """ govuk_status = GovUkPaymentStatus.get_from_govuk_payment(govuk_payment) if govuk_status is None or govuk_status.finished(): return govuk_status govuk_id = govuk_payment['payment_id'] response = requests.post( govuk_url(f'/payments/{govuk_id}/cancel'), headers=govuk_headers(), timeout=15, ) response.raise_for_status() govuk_status = GovUkPaymentStatus.cancelled govuk_payment['state']['status'] = govuk_status.name return govuk_status
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 test_confirmation_handles_govuk_errors(self): self.choose_debit_card_payment_method() self.fill_in_prisoner_details() self.fill_in_amount() with responses.RequestsMock() as rsps: ref = 'wargle-blargle' processor_id = '3' mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/%s/' % ref), json={ 'processor_id': processor_id, 'recipient_name': 'John', 'amount': 1700, 'status': 'pending', 'created': datetime.datetime.now().isoformat() + 'Z', }, status=200, ) rsps.add( rsps.GET, govuk_url('/payments/%s/' % processor_id), status=500 ) with self.patch_prisoner_details_check(), self.silence_logger(): response = self.client.get( self.url, {'payment_ref': ref}, follow=False ) self.assertContains(response, 'your payment could not be processed') self.assertContains(response, ref[:8].upper()) # check session is cleared for key in self.complete_session_keys: self.assertNotIn(key, self.client.session)
def test_captured_payment_with_captured_date_gets_updated(self): """ Test that when a MTP pending payment is captured, if the captured date is immediately available, the payment is marked as 'taken' and a confirmation email is sent. """ govuk_payment_data = { 'payment_id': PAYMENT_DATA['processor_id'], 'reference': PAYMENT_DATA['uuid'], 'state': {'status': 'capturable'}, 'email': '*****@*****.**', } with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': 1, 'results': [ { **PAYMENT_DATA, 'security_check': { 'status': 'accepted', 'user_actioned': True, }, }, ], }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), json=govuk_payment_data, status=200, ) # capture payment rsps.add( rsps.POST, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/capture/'), status=204, ) # get govuk payment to see if we have the captured date rsps.add( rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), json={ **govuk_payment_data, 'state': {'status': 'success'}, 'settlement_summary': { 'capture_submit_time': '2016-10-27T15:11:05Z', 'captured_date': '2016-10-27', }, }, status=200, ) # update status rsps.add( rsps.PATCH, api_url(f'/payments/{PAYMENT_DATA["uuid"]}/'), json={ **PAYMENT_DATA, 'status': 'taken', 'email': '*****@*****.**', }, status=200, ) call_command('update_incomplete_payments', verbosity=0) self.assertEqual(len(mail.outbox), 1) self.assertEqual( json.loads(rsps.calls[-1].request.body.decode()), { 'status': 'taken', 'received_at': '2016-10-27T15:11:05+00:00', }, )
def test_confirmation(self): self.choose_debit_card_payment_method() self.fill_in_prisoner_details() self.fill_in_amount() with responses.RequestsMock() as rsps: ref = 'wargle-blargle' processor_id = '3' mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/%s/' % ref), json={ 'processor_id': processor_id, 'recipient_name': 'John', 'amount': 1700, 'status': 'pending', 'created': datetime.datetime.now().isoformat() + 'Z', }, status=200, ) rsps.add( rsps.GET, govuk_url('/payments/%s/' % processor_id), json={ 'state': {'status': 'success'}, 'email': '*****@*****.**', '_links': { 'events': { 'method': 'GET', 'href': govuk_url('/payments/%s/events' % 1), } } }, status=200 ) rsps.add( rsps.GET, govuk_url('/payments/%s/events' % 1), json={ 'events': [ { 'state': {'status': 'success'}, 'updated': '2016-10-27T15:11:05.768Z' } ] }, status=200 ) rsps.add( rsps.PATCH, api_url('/payments/%s/' % ref), status=200, ) with self.patch_prisoner_details_check(): response = self.client.get( self.url, {'payment_ref': ref}, follow=False ) self.assertContains(response, 'success') # check session is cleared for key in self.complete_session_keys: self.assertNotIn(key, self.client.session) self.assertEqual('Send money to a prisoner: your payment was successful', mail.outbox[0].subject) self.assertTrue('WARGLE-B' in mail.outbox[0].body) self.assertTrue('£17' in mail.outbox[0].body) with self.patch_prisoner_details_check(): response = self.client.get( self.url, {'payment_ref': ref}, follow=True ) self.assertOnPage(response, 'choose_method')
def test_update_incomplete_payments_extracts_card_details(self): """ Test that card details are extracted from the GOV.UK payment and saved on the MTP payment. """ payment_extra_details = { 'cardholder_name': 'Jack Halls', 'card_brand': 'Visa', 'worldpay_id': '11112222-1111-2222-3333-111122223333', 'card_number_first_digits': '100002', 'card_number_last_digits': '1111', 'card_expiry_date': '11/18', 'billing_address': { 'line1': '102 Petty France', 'line2': '', 'city': 'London', 'postcode': 'SW1H9AJ', 'country': 'GB', }, } with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': 1, 'results': [PAYMENT_DATA], }, status=200, ) rsps.add( rsps.GET, govuk_url(f'/payments/{PAYMENT_DATA["processor_id"]}/'), json={ 'reference': PAYMENT_DATA['uuid'], 'state': {'status': 'success'}, 'settlement_summary': { 'capture_submit_time': '2016-10-27T15:11:05Z', 'captured_date': '2016-10-27', }, 'card_details': { 'card_brand': 'Visa', 'last_digits_card_number': '1111', 'first_digits_card_number': '100002', 'cardholder_name': 'Jack Halls', 'expiry_date': '11/18', 'billing_address': { 'line1': '102 Petty France', 'line2': '', 'postcode': 'SW1H9AJ', 'city': 'London', 'country': 'GB', }, }, 'provider_id': '11112222-1111-2222-3333-111122223333', 'email': '*****@*****.**', }, status=200, ) rsps.add( rsps.PATCH, api_url(f'/payments/{PAYMENT_DATA["uuid"]}/'), json={ **PAYMENT_DATA, **payment_extra_details, }, status=200, ) rsps.add( rsps.PATCH, api_url(f'/payments/{PAYMENT_DATA["uuid"]}/'), json={ **PAYMENT_DATA, **payment_extra_details, }, status=200, ) call_command('update_incomplete_payments', verbosity=0) self.assertDictEqual( json.loads(rsps.calls[-2].request.body.decode()), payment_extra_details, ) self.assertDictEqual( json.loads(rsps.calls[-1].request.body.decode()), { 'received_at': '2016-10-27T15:11:05+00:00', 'status': 'taken', }, )
def test_skip_payments(self): """ Test that checks for some payments get skipped and some get included - wargle-aaaa is a recent payment and the check is in pending so should be skipped - wargle-bbbb is an old payment so should be checked even if the check is in pending as it could have been timed out - wargle-cccc is a recent payment and the payment object doesn't have check info so should be included just in case - wargle-dddd is a recent payment and the check is in accepted so should be included - wargle-eeee is a recent payment and the check is in rejected so should be included """ payments = [ { 'uuid': 'wargle-aaaa', 'processor_id': 1, 'recipient_name': 'John', 'amount': 1700, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A1409AE', 'prisoner_dob': '1989-01-21', 'security_check': { 'status': 'pending', 'user_actioned': False, }, }, { 'uuid': 'wargle-bbbb', 'processor_id': 2, 'recipient_name': 'Tom', 'amount': 2000, 'status': 'pending', 'created': ( datetime.now() - ALWAYS_CHECK_IF_OLDER_THAN - timedelta(hours=1) ).isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A1234GJ', 'prisoner_dob': '1954-04-17', 'security_check': { 'status': 'pending', 'user_actioned': False, }, }, { 'uuid': 'wargle-cccc', 'processor_id': 3, 'recipient_name': 'Harry', 'amount': 500, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A5544CD', 'prisoner_dob': '1992-12-05', 'security_check': None, }, { 'uuid': 'wargle-dddd', 'processor_id': 4, 'recipient_name': 'Lisa', 'amount': 600, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A4444DB', 'prisoner_dob': '1992-12-05', 'security_check': { 'status': 'accepted', 'user_actioned': False, }, }, { 'uuid': 'wargle-eeee', 'processor_id': 5, 'recipient_name': 'Tom', 'amount': 700, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A4444DE', 'prisoner_dob': '1992-12-05', 'security_check': { 'status': 'rejected', 'user_actioned': True, }, }, ] with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': len(payments), 'results': payments, }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 2), json={ 'reference': 'wargle-bbbb', 'state': {'status': 'capturable'}, }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 3), json={ 'reference': 'wargle-cccc', 'state': {'status': 'submitted'}, 'settlement_summary': { 'capture_submit_time': '2016-10-27T15:11:05Z', 'captured_date': None }, }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 4), json={ 'reference': 'wargle-dddd', 'state': {'status': 'success'}, 'settlement_summary': { 'capture_submit_time': '2016-10-27T15:11:05Z', 'captured_date': '2016-10-27' }, 'email': '*****@*****.**', }, status=200, ) # save email rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-dddd'), json={ **payments[3], 'email': '*****@*****.**', }, status=200, ) # update status rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-dddd'), json={ **payments[3], 'email': '*****@*****.**', 'status': 'taken', }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 5), json={ 'reference': 'wargle-eeee', 'state': {'status': 'cancelled'}, 'email': '*****@*****.**', }, status=200, ) # update status rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-eeee'), json={ **payments[4], 'status': 'rejected', }, status=200, ) call_command('update_incomplete_payments', verbosity=0) # wargle-aaaa, wargle-bbbb and wargle-cccc already checked implicitly by RequestsMock # check wargle-dddd self.assertEqual( json.loads(rsps.calls[5].request.body.decode()), {'email': '*****@*****.**'}, ) self.assertDictEqual( json.loads(rsps.calls[6].request.body.decode()), { 'status': 'taken', 'received_at': '2016-10-27T15:11:05+00:00', }, ) self.assertEqual( mail.outbox[0].subject, 'Send money to someone in prison: your payment was successful', ) self.assertTrue('Lisa' in mail.outbox[0].body) self.assertTrue('£6' in mail.outbox[0].body) # check wargle-eeee self.assertEqual( json.loads(rsps.calls[8].request.body.decode()), { 'email': '*****@*****.**', 'status': 'rejected', }, ) self.assertEqual( mail.outbox[1].subject, 'Send money to someone in prison: your payment has NOT been sent to the prisoner', ) self.assertTrue('Tom' in mail.outbox[1].body) self.assertTrue('£7' in mail.outbox[1].body) # double-check that no more emails were sent self.assertEqual(len(mail.outbox), 2)
def test_update_incomplete_payments(self): """ Test that incomplete payments get updated appropriately. - wargle-1111 relates to a GOV.UK payment in 'success' status so * should become 'taken' * a confirmation email should be sent - wargle-2222 relates to a GOV.UK payment in 'submitted' status so should be ignored - wargle-3333 relates to a GOV.UK payment in 'failed' status without being in capturable status in the past so should become 'failed' - wargle-4444 relates to a GOV.UK payment in 'cancelled' status so: * should become 'rejected' * a notification email should be sent to the sender - wargle-5555 relates to a GOV.UK payment in 'failed' status which was in a capturable status in the past so: * should become 'expired' * a notification email should be sent to the sender - wargle-6666 relates to a GOV.UK payment in 'success' status after its credit was accepted by FIU so: * should become 'taken' * a different confirmation email should be sent - wargle-7777 relates to a GOV.UK payment in 'failed' status which was in a capturable status in the past and was rejected by FIU so: * should become 'expired' * a rejection email (not a timeout email) should be sent to the sender """ payments = [ { 'uuid': 'wargle-1111', 'processor_id': 1, 'recipient_name': 'John', 'amount': 1700, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A1409AE', 'prisoner_dob': '1989-01-21', 'security_check': { 'status': 'accepted', 'user_actioned': False, }, }, { 'uuid': 'wargle-2222', 'processor_id': 2, 'recipient_name': 'Tom', 'amount': 2000, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A1234GJ', 'prisoner_dob': '1954-04-17', 'security_check': { 'status': 'accepted', 'user_actioned': False, }, }, { 'uuid': 'wargle-3333', 'processor_id': 3, 'recipient_name': 'Harry', 'amount': 500, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A5544CD', 'prisoner_dob': '1992-12-05', 'security_check': { 'status': 'accepted', 'user_actioned': False, }, }, { 'uuid': 'wargle-4444', 'processor_id': 4, 'recipient_name': 'Lisa', 'amount': 600, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A4444DB', 'prisoner_dob': '1992-12-05', 'security_check': { 'status': 'accepted', 'user_actioned': False, }, }, { 'uuid': 'wargle-5555', 'processor_id': 5, 'recipient_name': 'Tom', 'amount': 700, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A4444DE', 'prisoner_dob': '1992-12-05', 'security_check': { 'status': 'accepted', 'user_actioned': False, }, }, { 'uuid': 'wargle-6666', 'processor_id': 6, 'recipient_name': 'Tim', 'amount': 800, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A1409AW', 'prisoner_dob': '1989-01-21', 'security_check': { 'status': 'accepted', 'user_actioned': True, }, }, { 'uuid': 'wargle-7777', 'processor_id': 7, 'recipient_name': 'Jim', 'amount': 900, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z', 'modified': datetime.now().isoformat() + 'Z', 'prisoner_number': 'A4444DQ', 'prisoner_dob': '1992-12-05', 'security_check': { 'status': 'rejected', 'user_actioned': True, }, }, ] with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': len(payments), 'results': payments, }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 1), json={ 'reference': 'wargle-1111', 'state': {'status': 'success'}, 'settlement_summary': { 'capture_submit_time': '2016-10-27T15:11:05Z', 'captured_date': '2016-10-27' }, 'email': '*****@*****.**', }, status=200, ) # save email rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-1111'), json={ **payments[0], 'email': '*****@*****.**', }, status=200, ) # update status rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-1111'), json={ **payments[0], 'email': '*****@*****.**', 'status': 'taken', }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 2), json={ 'reference': 'wargle-2222', 'state': {'status': 'submitted'}, 'settlement_summary': { 'capture_submit_time': '2016-10-27T15:11:05Z', 'captured_date': None }, 'email': '*****@*****.**', }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 3), json={ 'reference': 'wargle-3333', 'state': {'status': 'failed'}, 'settlement_summary': { 'capture_submit_time': None, 'captured_date': None }, 'email': '*****@*****.**', }, status=200, ) # update status rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-3333'), json={ **payments[2], 'status': 'failed', }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 4), json={ 'reference': 'wargle-4444', 'state': {'status': 'cancelled'}, 'email': '*****@*****.**', }, status=200, ) # update status rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-4444'), json={ **payments[3], 'status': 'rejected', }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 5), json={ 'payment_id': 'wargle-5555', 'reference': 'wargle-5555', 'state': { 'status': 'failed', 'code': 'P0020', }, 'email': '*****@*****.**', }, status=200, ) # get govuk payment events rsps.add( rsps.GET, govuk_url('/payments/%s/events/' % 'wargle-5555'), status=200, json={ 'events': [ { 'payment_id': 'wargle-5555', 'state': { 'status': 'capturable', 'finished': False, }, }, ], 'payment_id': 'wargle-5555', }, ) # update status rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-5555'), json={ **payments[4], 'status': 'expired', }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 6), json={ 'reference': 'wargle-6666', 'state': {'status': 'success'}, 'settlement_summary': { 'capture_submit_time': '2016-10-27T15:11:05Z', 'captured_date': '2016-10-27' }, 'email': '*****@*****.**', }, status=200, ) # save email rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-6666'), json={ **payments[5], 'email': '*****@*****.**', }, status=200, ) # update status rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-6666'), json={ **payments[5], 'status': 'taken', }, status=200, ) # get govuk payment rsps.add( rsps.GET, govuk_url('/payments/%s/' % 7), json={ 'payment_id': 'wargle-7777', 'reference': 'wargle-7777', 'state': { 'status': 'failed', 'code': 'P0020', }, 'email': '*****@*****.**', }, status=200, ) # get govuk payment events rsps.add( rsps.GET, govuk_url('/payments/%s/events/' % 'wargle-7777'), status=200, json={ 'events': [ { 'payment_id': 'wargle-7777', 'state': { 'status': 'capturable', 'finished': False, }, }, ], 'payment_id': 'wargle-7777', }, ) # update status rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-7777'), json={ **payments[0], 'status': 'expired', }, status=200, ) call_command('update_incomplete_payments', verbosity=0) # check wargle-1111 self.assertEqual( json.loads(rsps.calls[3].request.body.decode()), {'email': '*****@*****.**'}, ) self.assertDictEqual( json.loads(rsps.calls[4].request.body.decode()), { 'status': 'taken', 'received_at': '2016-10-27T15:11:05+00:00', }, ) self.assertEqual( mail.outbox[0].subject, 'Send money to someone in prison: your payment was successful', ) self.assertTrue('John' in mail.outbox[0].body) self.assertTrue('£17' in mail.outbox[0].body) # check wargle-3333 self.assertEqual( json.loads(rsps.calls[7].request.body.decode()), { 'email': '*****@*****.**', 'status': 'failed', }, ) # check wargle-4444 self.assertEqual( json.loads(rsps.calls[9].request.body.decode()), { 'email': '*****@*****.**', 'status': 'rejected', }, ) self.assertEqual( mail.outbox[1].subject, 'Send money to someone in prison: your payment has NOT been sent to the prisoner', ) self.assertTrue('Lisa' in mail.outbox[1].body) self.assertTrue('£6' in mail.outbox[1].body) # check wargle-5555 self.assertEqual( json.loads(rsps.calls[12].request.body.decode()), { 'email': '*****@*****.**', 'status': 'expired', }, ) self.assertEqual( mail.outbox[2].subject, 'Send money to someone in prison: payment session expired', ) self.assertTrue('Tom' in mail.outbox[2].body) self.assertTrue('£7' in mail.outbox[2].body) # check wargle-6666 self.assertEqual( json.loads(rsps.calls[14].request.body.decode()), {'email': '*****@*****.**'}, ) self.assertDictEqual( json.loads(rsps.calls[15].request.body.decode()), { 'status': 'taken', 'received_at': '2016-10-27T15:11:05+00:00', }, ) self.assertEqual( mail.outbox[3].subject, 'Send money to someone in prison: your payment has now gone through', ) self.assertTrue('Tim' in mail.outbox[3].body) self.assertTrue('£8' in mail.outbox[3].body) # check wargle-7777 self.assertEqual( json.loads(rsps.calls[18].request.body.decode()), { 'email': '*****@*****.**', 'status': 'expired', }, ) self.assertEqual( mail.outbox[4].subject, 'Send money to someone in prison: your payment has NOT been sent to the prisoner', ) self.assertTrue('Jim' in mail.outbox[4].body) self.assertTrue('£9' in mail.outbox[4].body) # double-check that no more emails were sent self.assertEqual(len(mail.outbox), 5)
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_update_incomplete_payments(self): with responses.RequestsMock() as rsps: mock_auth(rsps) rsps.add( rsps.GET, api_url('/payments/'), json={ 'count': 3, 'results': [ { 'uuid': 'wargle-1111', 'processor_id': 1, 'recipient_name': 'John', 'amount': 1700, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z' }, { 'uuid': 'wargle-2222', 'processor_id': 2, 'recipient_name': 'Tom', 'amount': 2000, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z' }, { 'uuid': 'wargle-3333', 'processor_id': 3, 'recipient_name': 'Harry', 'amount': 500, 'status': 'pending', 'created': datetime.now().isoformat() + 'Z' }, ] }, status=200, ) rsps.add( rsps.GET, govuk_url('/payments/%s/' % 1), json={ 'state': {'status': 'success'}, 'email': '*****@*****.**', '_links': { 'events': { 'method': 'GET', 'href': govuk_url('/payments/%s/events' % 1), } } }, status=200 ) rsps.add( rsps.GET, govuk_url('/payments/%s/events' % 1), json={ 'events': [ { 'state': {'status': 'success'}, 'updated': '2016-10-27T15:11:05.768Z' } ] }, status=200 ) rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-1111'), status=200, ) rsps.add( rsps.GET, govuk_url('/payments/%s/' % 2), json={ 'state': {'status': 'submitted'}, 'email': '*****@*****.**', }, status=200 ) rsps.add( rsps.GET, govuk_url('/payments/%s/' % 3), json={ 'state': {'status': 'failed'}, 'email': '*****@*****.**', }, status=200 ) rsps.add( rsps.PATCH, api_url('/payments/%s/' % 'wargle-3333'), status=200, ) call_command('update_incomplete_payments')
def test_success_page(self): ref = 'f469ec29-fb86-40db-a9e0-6faa409533be' processor_id = '3' with responses.RequestsMock( ) as rsps, self.patch_view_chain_form_checking(): mock_auth(rsps) rsps.add(rsps.GET, api_url('/payments/%s/' % ref), json={ 'uuid': ref, 'processor_id': processor_id, 'recipient_name': 'James Bond', 'amount': 2000, 'status': 'pending', 'created': datetime.datetime.now().isoformat() + 'Z', 'prisoner_number': 'A5544CD', 'prisoner_dob': '1992-12-05', }) rsps.add(rsps.GET, govuk_url('/payments/%s' % processor_id), json={ 'reference': ref, 'state': { 'status': 'success', 'finished': True }, 'amount': 2000, 'payment_id': processor_id, 'email': '*****@*****.**', '_links': { 'events': { 'href': govuk_url('/payments/%s/events' % processor_id), 'method': 'GET' } } }) rsps.add(rsps.PATCH, api_url('/payments/%s/' % ref), json={ 'uuid': ref, 'processor_id': processor_id, 'recipient_name': 'James Bond', 'amount': 2000, 'status': 'pending', 'created': datetime.datetime.now().isoformat() + 'Z', 'prisoner_number': 'A5544CD', 'prisoner_dob': '1992-12-05', 'email': '*****@*****.**', }) self.driver.get(self.live_server_url + '/en-gb/debit-card/confirmation/?payment_ref=' + ref) self.assertInSource('Payment successful') self.assertInSource('<strong>F469EC29</strong>') self.assertInSource('James Bond') self.assertInSource('£20') self.assertInSource('Print this page')