def test_notify_on_quote_cancelled(self, mocked_notify_client): """Test that a notification is triggered when a quote is cancelled.""" order = OrderWithOpenQuoteFactory(assignees=[]) OrderAssigneeFactory.create_batch(1, order=order) OrderSubscriberFactory.create_batch(2, order=order) mocked_notify_client.reset_mock() order.reopen(by=AdviserFactory()) # 1 = customer, 3 = assignees/subscribers assert len( mocked_notify_client.send_email_notification.call_args_list) == ( 3 + 1) templates_called = [ data[1]['template_id'] for data in mocked_notify_client.send_email_notification.call_args_list ] assert templates_called == [ Template.quote_cancelled_for_customer.value, Template.quote_cancelled_for_adviser.value, Template.quote_cancelled_for_adviser.value, Template.quote_cancelled_for_adviser.value, ]
def test_advisers_notified(self): """ Test that calling `quote_generated` sends an email to all advisers notifying them that the quote has been sent. """ order = OrderWithOpenQuoteFactory(assignees=[]) assignees = OrderAssigneeFactory.create_batch(2, order=order) subscribers = OrderSubscriberFactory.create_batch(2, order=order) notify.client.reset_mock() notify.quote_generated(order) assert notify.client.send_email_notification.called # 1 = customer, 4 = assignees/subscribers assert len( notify.client.send_email_notification.call_args_list) == (4 + 1) calls_by_email = { data['email_address']: { 'template_id': data['template_id'], 'personalisation': data['personalisation'], } for _, data in notify.client.send_email_notification.call_args_list } for item in itertools.chain(assignees, subscribers): call = calls_by_email[item.adviser.get_current_email()] assert call['template_id'] == Template.quote_sent_for_adviser.value assert call['personalisation'][ 'recipient name'] == item.adviser.name assert call['personalisation'][ 'embedded link'] == order.get_datahub_frontend_url()
def test_get(self): """Test a successful call to get a quote.""" order = OrderWithOpenQuoteFactory() quote = order.quote url = reverse('api-v3:omis:quote:detail', kwargs={'order_pk': order.pk}) response = self.api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.json() == { 'created_on': format_date_or_datetime(quote.created_on), 'created_by': { 'id': str(quote.created_by.pk), 'first_name': quote.created_by.first_name, 'last_name': quote.created_by.last_name, 'name': quote.created_by.name, }, 'cancelled_on': None, 'cancelled_by': None, 'accepted_on': None, 'accepted_by': None, 'expires_on': quote.expires_on.isoformat(), 'content': quote.content, 'terms_and_conditions': TermsAndConditions.objects.first().content, }
def test_accept(self): """Test that a quote can get accepted.""" order = OrderWithOpenQuoteFactory() quote = order.quote url = reverse( f'api-v3:omis-public:quote:accept', kwargs={'public_token': order.public_token}, ) client = self.create_api_client( scope=Scope.public_omis_front_end, grant_type=Application.GRANT_CLIENT_CREDENTIALS, ) with freeze_time('2017-07-12 13:00'): response = client.post(url) assert response.status_code == status.HTTP_200_OK assert response.json() == { 'created_on': format_date_or_datetime(quote.created_on), 'accepted_on': format_date_or_datetime(now()), 'cancelled_on': None, 'expires_on': quote.expires_on.isoformat(), 'content': quote.content, 'terms_and_conditions': TermsAndConditions.objects.first().content, } quote.refresh_from_db() assert quote.is_accepted() assert quote.accepted_on == now()
def test_with_active_quote(self): """ Test that if an order with an active quote is reopened, the quote is cancelled. """ order = OrderWithOpenQuoteFactory() assert not order.quote.is_cancelled() adviser = AdviserFactory() with freeze_time('2017-07-12 13:00'): order.reopen(by=adviser) assert order.quote.is_cancelled() assert order.quote.cancelled_by == adviser assert order.quote.cancelled_on == now() assert order.status == OrderStatus.draft
def test_accept(self, public_omis_api_client): """Test that a quote can get accepted.""" order = OrderWithOpenQuoteFactory() quote = order.quote url = reverse( 'api-v3:public-omis:quote:accept', kwargs={'public_token': order.public_token}, ) with freeze_time('2017-07-12 13:00'): response = public_omis_api_client.post(url, json_={}) assert response.status_code == status.HTTP_200_OK assert response.json() == { 'created_on': format_date_or_datetime(quote.created_on), 'accepted_on': format_date_or_datetime(now()), 'cancelled_on': None, 'expires_on': quote.expires_on.isoformat(), 'content': quote.content, 'terms_and_conditions': TermsAndConditions.objects.first().content, } quote.refresh_from_db() assert quote.is_accepted() assert quote.accepted_on == now()
def test_with_existing_active_quote(self): """Test that if there's already an active quote, the validation fails.""" order = OrderWithOpenQuoteFactory() validator = NoOtherActiveQuoteExistsSubValidator() with pytest.raises(APIConflictException): validator(order=order)
def test_quote_cancelled(self, end_to_end_notify, notify_task_return_value_tracker): """ Test templates of quote cancelled for customer and advisers. If the template variables have been changed in GOV.UK notifications the celery task will be unsuccessful. """ order = OrderWithOpenQuoteFactory() end_to_end_notify.quote_cancelled(order, by=AdviserFactory()) self._assert_tasks_successful(2, notify_task_return_value_tracker)
def test_customer_notified(self): """ Test that calling `quote_generated` sends an email notifying the customer that they have to accept the quote. """ order = OrderWithOpenQuoteFactory() notify.client.reset_mock() notify.quote_generated(order) assert notify.client.send_email_notification.called call_args = notify.client.send_email_notification.call_args_list[0][1] assert call_args['email_address'] == order.get_current_contact_email() assert call_args[ 'template_id'] == Template.quote_sent_for_customer.value assert call_args['personalisation'][ 'recipient name'] == order.contact.name assert call_args['personalisation'][ 'embedded link'] == order.get_public_facing_url()
def test_quote_cancelled(self, settings): """ Test templates of quote cancelled for customer and advisers. If the template variables have been changed in GOV.UK notifications this is going to raise HTTPError (400 - Bad Request). """ settings.OMIS_NOTIFICATION_API_KEY = settings.OMIS_NOTIFICATION_TEST_API_KEY notify = Notify() order = OrderWithOpenQuoteFactory() notify.quote_cancelled(order, by=AdviserFactory())
def test_409_if_theres_already_a_valid_quote(self, quote_view_name): """Test that if the order has already an active quote, the endpoint returns 409.""" order = OrderWithOpenQuoteFactory() url = reverse( f'api-v3:omis:quote:{quote_view_name}', kwargs={'order_pk': order.pk}, ) response = self.api_client.post(url) assert response.status_code == status.HTTP_409_CONFLICT assert response.json() == {'detail': "There's already an active quote."}
def test_accepting_quote_updates_opensearch(opensearch_with_signals): """ Test that when a quote is accepted and the invoice created, the payment_due_date field in OpenSearch gets updated. """ order = OrderWithOpenQuoteFactory() opensearch_with_signals.indices.refresh() result = opensearch_with_signals.get( index=OrderSearchApp.search_model.get_write_index(), id=order.pk, ) assert not result['_source']['payment_due_date'] order.accept_quote(by=None) opensearch_with_signals.indices.refresh() result = opensearch_with_signals.get( index=OrderSearchApp.search_model.get_write_index(), id=order.pk, ) assert result['_source'][ 'payment_due_date'] == order.invoice.payment_due_date.isoformat()
def test_accepting_quote_updates_es(setup_es): """ Test that when a quote is accepted and the invoice created, the payment_due_date field in ES gets updated. """ order = OrderWithOpenQuoteFactory() setup_es.indices.refresh() result = setup_es.get( index=OrderSearchApp.es_model.get_write_index(), doc_type=OrderSearchApp.name, id=order.pk, ) assert not result['_source']['payment_due_date'] order.accept_quote(by=None) setup_es.indices.refresh() result = setup_es.get( index=OrderSearchApp.es_model.get_write_index(), doc_type=OrderSearchApp.name, id=order.pk, ) assert result['_source']['payment_due_date'] == order.invoice.payment_due_date.isoformat()
def test_notify_on_quote_accepted(self): """Test that a notification is triggered when a quote is accepted.""" order = OrderWithOpenQuoteFactory(assignees=[]) OrderAssigneeFactory.create_batch(1, order=order, is_lead=True) OrderSubscriberFactory.create_batch(2, order=order) notify.client.reset_mock() order.accept_quote(by=None) # 1 = customer, 3 = assignees/subscribers assert len( notify.client.send_email_notification.call_args_list) == (3 + 1) templates_called = [ data[1]['template_id'] for data in notify.client.send_email_notification.call_args_list ] assert templates_called == [ Template.quote_accepted_for_customer.value, Template.quote_accepted_for_adviser.value, Template.quote_accepted_for_adviser.value, Template.quote_accepted_for_adviser.value, ]
def test_atomicity(self): """Test that if there's a problem with saving the order, the quote is not saved either.""" order = OrderWithOpenQuoteFactory() with mock.patch.object(order, 'save') as mocked_save: mocked_save.side_effect = Exception() with pytest.raises(Exception): order.accept_quote(by=None) quote = order.quote order.refresh_from_db() quote.refresh_from_db() assert not quote.is_accepted() assert not order.invoice assert not Invoice.objects.count()
def test_with_open_quote(self): """Test that if the quote is open, it gets cancelled.""" order = OrderWithOpenQuoteFactory() quote = order.quote url = reverse( f'api-v3:omis:quote:cancel', kwargs={'order_pk': order.pk}, ) with freeze_time('2017-07-12 13:00') as mocked_now: response = self.api_client.post(url) assert response.status_code == status.HTTP_200_OK assert response.json() == { 'created_on': format_date_or_datetime(quote.created_on), 'created_by': { 'id': str(quote.created_by.pk), 'first_name': quote.created_by.first_name, 'last_name': quote.created_by.last_name, 'name': quote.created_by.name, }, 'cancelled_on': format_date_or_datetime(mocked_now()), 'cancelled_by': { 'id': str(self.user.pk), 'first_name': self.user.first_name, 'last_name': self.user.last_name, 'name': self.user.name, }, 'accepted_on': None, 'accepted_by': None, 'expires_on': quote.expires_on.isoformat(), 'content': quote.content, 'terms_and_conditions': TermsAndConditions.objects.first().content, } quote.refresh_from_db() assert quote.is_cancelled()
def test_ok_if_order_in_allowed_status(self, allowed_status): """ Test that the quote of an order can be accepted if the order is in one of the allowed statuses. """ order = OrderWithOpenQuoteFactory(status=allowed_status) contact = ContactFactory() order.accept_quote(by=contact) order.refresh_from_db() assert order.status == OrderStatus.quote_accepted assert order.quote.accepted_on assert order.quote.accepted_by == contact assert order.invoice assert order.invoice.billing_company_name == order.billing_company_name assert order.invoice.billing_address_1 == order.billing_address_1 assert order.invoice.billing_address_2 == order.billing_address_2 assert order.invoice.billing_address_town == order.billing_address_town assert order.invoice.billing_address_county == order.billing_address_county assert order.invoice.billing_address_postcode == order.billing_address_postcode assert order.invoice.billing_address_country == order.billing_address_country assert order.invoice.po_number == order.po_number assert order.invoice.contact_email == order.get_current_contact_email()
class TestRefundAdmin(AdminTestMixin): """Tests for the Refund Admin.""" def test_add(self): """ Test adding a refund with status 'Approved'. This is the only status allowed when creating a record at the moment. """ order = OrderPaidFactory() now_datetime = now() now_date_str = now_datetime.date().isoformat() now_time_str = now_datetime.time().isoformat() assert Refund.objects.count() == 0 url = reverse('admin:omis_payment_refund_add') data = { 'order': order.pk, 'status': RefundStatus.APPROVED, 'requested_on_0': now_date_str, 'requested_on_1': now_time_str, 'requested_by': AdviserFactory().pk, 'requested_amount': order.total_cost, 'refund_reason': 'lorem ipsum refund reason', 'level1_approved_on_0': now_date_str, 'level1_approved_on_1': now_time_str, 'level1_approved_by': AdviserFactory().pk, 'level1_approval_notes': 'lorem ipsum level 1', 'level2_approved_on_0': now_date_str, 'level2_approved_on_1': now_time_str, 'level2_approved_by': AdviserFactory().pk, 'level2_approval_notes': 'lorem ipsum level 2', 'method': PaymentMethod.BACS, 'net_amount': order.total_cost - 1, 'vat_amount': 1, 'additional_reference': 'additional reference', 'rejection_reason': 'lorem ipsum rejection reason', } response = self.client.post(url, data, follow=True) assert response.status_code == status.HTTP_200_OK assert Refund.objects.count() == 1 refund = Refund.objects.first() assert refund.order.pk == data['order'] assert refund.status == data['status'] assert refund.requested_on == now_datetime assert refund.requested_by.pk == data['requested_by'] assert refund.requested_amount == data['requested_amount'] assert refund.refund_reason == data['refund_reason'] assert refund.level1_approved_on == now_datetime assert refund.level1_approved_by.pk == data['level1_approved_by'] assert refund.level1_approval_notes == data['level1_approval_notes'] assert refund.level2_approved_on == now_datetime assert refund.level2_approved_by.pk == data['level2_approved_by'] assert refund.level2_approval_notes == data['level2_approval_notes'] assert refund.method == data['method'] assert refund.net_amount == data['net_amount'] assert refund.vat_amount == data['vat_amount'] assert refund.additional_reference == data['additional_reference'] assert refund.rejection_reason == data['rejection_reason'] assert refund.total_amount == order.total_cost assert refund.created_by == self.user assert refund.modified_by == self.user assert not refund.payment @pytest.mark.parametrize( 'refund_factory', ( RequestedRefundFactory, ApprovedRefundFactory, RejectedRefundFactory, ), ) def test_change(self, refund_factory): """Test changing a refund record, its status cannot change at this point.""" refund = refund_factory() order = OrderPaidFactory() now_datetime = now() now_date_str = now_datetime.date().isoformat() now_time_str = now_datetime.time().isoformat() url = reverse('admin:omis_payment_refund_change', args=(refund.id, )) data = { 'order': order.pk, 'status': refund.status, 'requested_on_0': now_date_str, 'requested_on_1': now_time_str, 'requested_by': AdviserFactory().pk, 'requested_amount': order.total_cost, 'refund_reason': 'lorem ipsum refund reason', 'level1_approved_on_0': now_date_str, 'level1_approved_on_1': now_time_str, 'level1_approved_by': AdviserFactory().pk, 'level1_approval_notes': 'lorem ipsum level 1', 'level2_approved_on_0': now_date_str, 'level2_approved_on_1': now_time_str, 'level2_approved_by': AdviserFactory().pk, 'level2_approval_notes': 'lorem ipsum level 2', 'method': PaymentMethod.BACS, 'net_amount': order.total_cost - 1, 'vat_amount': 1, 'additional_reference': 'additional reference', 'rejection_reason': 'lorem ipsum rejection reason', } response = self.client.post(url, data, follow=True) assert response.status_code == status.HTTP_200_OK refund.refresh_from_db() assert refund.order.pk == data['order'] assert refund.status == data['status'] assert refund.requested_on == now_datetime assert refund.requested_by.pk == data['requested_by'] assert refund.requested_amount == data['requested_amount'] assert refund.refund_reason == data['refund_reason'] assert refund.level1_approved_on == now_datetime assert refund.level1_approved_by.pk == data['level1_approved_by'] assert refund.level1_approval_notes == data['level1_approval_notes'] assert refund.level2_approved_on == now_datetime assert refund.level2_approved_by.pk == data['level2_approved_by'] assert refund.level2_approval_notes == data['level2_approval_notes'] assert refund.method == data['method'] assert refund.net_amount == data['net_amount'] assert refund.vat_amount == data['vat_amount'] assert refund.additional_reference == data['additional_reference'] assert refund.rejection_reason == data['rejection_reason'] assert refund.total_amount == order.total_cost assert refund.created_by != self.user assert refund.modified_by == self.user assert not refund.payment @pytest.mark.parametrize( 'data_delta,errors', ( # invalid status ( { 'status': RefundStatus.REJECTED }, { 'status': [ 'Select a valid choice. rejected is not one of the available choices.', ], }, ), # invalid order status ( { 'order': lambda *_: OrderWithOpenQuoteFactory() }, { 'order': ['This order has not been paid for.'] }, ), # requested on < order.paid_on ( { 'order': lambda *_: OrderPaidFactory(paid_on=dateutil_parse( '2018-01-01T13:00Z'), ), 'requested_on_0': '2018-01-01', 'requested_on_1': '12:59', }, { 'requested_on': [ 'Please specify a value greater than or equal to Jan. 1, 2018, 1 p.m..', ], }, ), # level1 approved on < order.paid_on ( { 'order': lambda *_: OrderPaidFactory(paid_on=dateutil_parse( '2018-01-01T13:00Z'), ), 'level1_approved_on_0': '2018-01-01', 'level1_approved_on_1': '12:59', }, { 'level1_approved_on': [ 'Please specify a value greater than or equal to Jan. 1, 2018, 1 p.m..', ], }, ), # level2 approved on < order.paid_on ( { 'order': lambda *_: OrderPaidFactory(paid_on=dateutil_parse( '2018-01-01T13:00Z'), ), 'level2_approved_on_0': '2018-01-01', 'level2_approved_on_1': '12:59', }, { 'level2_approved_on': [ 'Please specify a value greater than or equal to Jan. 1, 2018, 1 p.m..', ], }, ), # same level1 and level2 approver ( { 'level1_approved_by': lambda *_: AdviserFactory().pk, 'level2_approved_by': lambda _, d: d['level1_approved_by'], }, { 'level1_approved_by': ['Approvers level1 and level2 have to be different.'], }, ), # net_amount + vat_amount > order.total_cost ( { 'net_amount': lambda o, _: o.total_cost, 'vat_amount': lambda *_: 1, }, { 'net_amount': lambda o, _: [ f'Remaining amount that can be refunded: {o.total_cost}.', ], }, ), ), ) def test_validation_error(self, data_delta, errors): """Test validation errors.""" def resolve(value, order, data): if callable(value): return value(order, data) return value order = data_delta.pop('order', None) or OrderPaidFactory() order = resolve(order, None, None) now_datetime = now() now_date_str = now_datetime.date().isoformat() now_time_str = now_datetime.time().isoformat() url = reverse('admin:omis_payment_refund_add') data = { 'order': order.pk, 'status': RefundStatus.APPROVED, 'requested_on_0': now_date_str, 'requested_on_1': now_time_str, 'requested_by': AdviserFactory().pk, 'requested_amount': order.total_cost, 'refund_reason': 'lorem ipsum refund reason', 'level1_approved_on_0': now_date_str, 'level1_approved_on_1': now_time_str, 'level1_approved_by': AdviserFactory().pk, 'level1_approval_notes': 'lorem ipsum level 1', 'level2_approved_on_0': now_date_str, 'level2_approved_on_1': now_time_str, 'level2_approved_by': AdviserFactory().pk, 'level2_approval_notes': 'lorem ipsum level 2', 'method': PaymentMethod.BACS, 'net_amount': order.total_cost - 1, 'vat_amount': 1, 'additional_reference': 'additional reference', } for data_key, data_value in data_delta.items(): data[data_key] = resolve(data_value, order, data) response = self.client.post(url, data, follow=True) assert response.status_code == status.HTTP_200_OK form = response.context['adminform'].form assert not form.is_valid() for error_key, error_value in errors.items(): errors[error_key] = resolve(error_value, order, errors) assert form.errors == errors @pytest.mark.parametrize( 'refund_factory,required_fields', ( ( RequestedRefundFactory, ( 'order', 'status', 'requested_on', 'requested_amount', ), ), ( ApprovedRefundFactory, ( 'order', 'status', 'requested_on', 'requested_amount', 'level1_approved_on', 'level1_approved_by', 'level2_approved_on', 'level2_approved_by', 'method', 'net_amount', 'vat_amount', ), ), ( RejectedRefundFactory, ( 'order', 'status', 'requested_on', 'requested_amount', ), ), ), ) def test_required_fields(self, refund_factory, required_fields): """Test required fields depending on the status of the refund.""" refund = refund_factory() url = reverse('admin:omis_payment_refund_change', args=(refund.id, )) data = { 'order': '', 'status': '', 'requested_on_0': '', 'requested_on_1': '', 'requested_by': '', 'requested_amount': '', 'refund_reason': '', 'level1_approved_on_0': '', 'level1_approved_on_1': '', 'level1_approved_by': '', 'level1_approval_notes': '', 'level2_approved_on_0': '', 'level2_approved_on_1': '', 'level2_approved_by': '', 'level2_approval_notes': '', 'method': '', 'net_amount': '', 'vat_amount': '', 'additional_reference': '', 'rejection_reason': '', } response = self.client.post(url, data, follow=True) form = response.context['adminform'].form assert not form.is_valid() assert form.errors == { required_field: ['This field is required.'] for required_field in required_fields } @pytest.mark.parametrize( 'refund_factory', ( RequestedRefundFactory, ApprovedRefundFactory, RejectedRefundFactory, ), ) def test_cannot_change_status(self, refund_factory): """Test that the status field cannot be changed at any point.""" refund = refund_factory() now_datetime = now() date_str = now_datetime.date().isoformat() time_str = now_datetime.time().isoformat() url = reverse('admin:omis_payment_refund_change', args=(refund.id, )) default_data = { 'order': refund.order.pk, 'requested_on_0': date_str, 'requested_on_1': time_str, 'requested_amount': refund.requested_amount, 'refund_reason': refund.refund_reason, 'level1_approved_on_0': date_str, 'level1_approved_on_1': time_str, 'level1_approved_by': AdviserFactory().pk, 'level2_approved_on_0': date_str, 'level2_approved_on_1': time_str, 'level2_approved_by': AdviserFactory().pk, 'method': refund.method or '', 'net_amount': '' if refund.net_amount is None else refund.net_amount, 'vat_amount': '' if refund.vat_amount is None else refund.vat_amount, } for changed_status, _ in RefundStatus.choices: if changed_status == refund.status: continue data = { **default_data, 'status': changed_status, } response = self.client.post(url, data, follow=True) assert response.status_code == status.HTTP_200_OK form = response.context['adminform'].form assert not form.is_valid() assert form.errors == { 'status': [ f'Select a valid choice. {changed_status} is not one of the available ' f'choices.', ], }