def test_advisers_notified(self): """ Test that calling `quote_accepted` sends an email to all advisers notifying them that the quote has been accepted. """ order = OrderPaidFactory(assignees=[]) assignees = OrderAssigneeFactory.create_batch(2, order=order) subscribers = OrderSubscriberFactory.create_batch(2, order=order) notify.client.reset_mock() notify.quote_accepted(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_accepted_for_adviser.value assert call['personalisation'][ 'recipient name'] == item.adviser.name assert call['personalisation'][ 'embedded link'] == order.get_datahub_frontend_url()
def test_validation_error_if_not_all_actual_time_set(self): """ Test that if not all assignee actual time fields have been set, a validation error is raised and the call fails. """ order = OrderPaidFactory(assignees=[]) OrderAssigneeCompleteFactory(order=order) OrderAssigneeFactory(order=order) with pytest.raises(ValidationError): order.complete(by=None)
def test_create_from_order( self, mocked_generate_datetime_based_reference, ): """Test that Payment.objects.create_from_order creates a payment.""" mocked_generate_datetime_based_reference.return_value = '201702010004' order = OrderPaidFactory() by = AdviserFactory() attrs = { 'transaction_reference': 'lorem ipsum', 'amount': 1001, 'received_on': dateutil_parse('2017-01-01').date(), } payment = Payment.objects.create_from_order( order=order, by=by, attrs=attrs, ) payment.refresh_from_db() assert payment.reference == '201702010004' assert payment.created_by == by assert payment.order == order assert payment.transaction_reference == attrs['transaction_reference'] assert payment.additional_reference == '' assert payment.amount == attrs['amount'] assert payment.received_on == attrs['received_on']
def test_400_if_readonly_fields_changed(self, data): """ Test that estimated_time and is_lead cannot be set at this stage. """ order = OrderPaidFactory(assignees=[]) assignee = OrderAssigneeFactory(order=order) url = reverse( 'api-v3:omis:order:assignee', kwargs={'order_pk': order.id}, ) response = self.api_client.patch( url, [ { 'adviser': { 'id': assignee.adviser.id }, **data, }, ], ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == [ { list(data)[0]: [ 'This field cannot be changed at this stage.', ], }, ]
def test_400_if_assignee_added_with_extra_field(self, data): """ Test that estimated_time and is_lead cannot be set at this stage even when adding a new assignee. """ order = OrderPaidFactory() new_adviser = AdviserFactory() url = reverse( 'api-v3:omis:order:assignee', kwargs={'order_pk': order.id}, ) response = self.api_client.patch( url, [ { 'adviser': { 'id': new_adviser.id, }, **data, }, ], ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == [ { list(data)[0]: [ 'This field cannot be changed at this stage.', ], }, ]
def test_get(self): """Test a successful call to get a list of payments.""" order = OrderPaidFactory() PaymentFactory.create_batch(2, order=order) PaymentFactory.create_batch( 5) # create some extra ones not linked to `order` url = reverse('api-v3:omis:payment:collection', 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(payment.created_on), 'reference': payment.reference, 'transaction_reference': payment.transaction_reference, 'additional_reference': payment.additional_reference, 'amount': payment.amount, 'method': payment.method, 'received_on': payment.received_on.isoformat(), } for payment in order.payments.all()]
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
def test_verbs_not_allowed(self, verb, public_omis_api_client): """Test that makes sure the other verbs are not allowed.""" order = OrderPaidFactory() url = reverse( 'api-v3:public-omis:payment:collection', kwargs={'public_token': order.public_token}, ) response = getattr(public_omis_api_client, verb)(url, json_={}) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
def test_empty_list(self): """Test that if no payments exist, the endpoint returns an empty list.""" order = OrderPaidFactory() PaymentFactory.create_batch(5) # create some payments not linked to `order` url = reverse('api-v3:omis:payment:collection', kwargs={'order_pk': order.pk}) response = self.api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.json() == []
def test_quote_accepted(self, end_to_end_notify, notify_task_return_value_tracker): """ Test templates of quote accepted for customer and advisers. If the template variables have been changed in GOV.UK notifications the celery task will be unsuccessful. """ order = OrderPaidFactory() end_to_end_notify.quote_accepted(order) self._assert_tasks_successful(2, notify_task_return_value_tracker)
def test_customer_notified(self): """ Test that calling `quote_accepted` sends an email notifying the customer that they have accepted the quote. """ order = OrderPaidFactory() notify.client.reset_mock() notify.quote_accepted(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_accepted_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_accepted(self, settings): """ Test templates of quote accepted 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 = OrderPaidFactory() notify.quote_accepted(order)
def test_notify_on_order_completed(self): """Test that a notification is triggered when an order is marked as completed.""" order = OrderPaidFactory(assignees=[]) OrderAssigneeCompleteFactory.create_batch(1, order=order, is_lead=True) OrderSubscriberFactory.create_batch(2, order=order) notify.client.reset_mock() order.complete(by=None) # 3 = assignees/subscribers assert len(notify.client.send_email_notification.call_args_list) == 3 templates_called = [ data[1]['template_id'] for data in notify.client.send_email_notification.call_args_list ] assert templates_called == [ Template.order_completed_for_adviser.value, Template.order_completed_for_adviser.value, Template.order_completed_for_adviser.value, ]
def test_400_if_assignee_deleted(self): """ Test that assignees cannot be deleted at this stage. Given an order with the following assignees: [ { "adviser": {"id": 1}, "estimated_time": 100, "is_lead": true }, { "adviser": {"id": 2}, "estimated_time": 250, "is_lead": false }, ] if I pass the following data with force_delete == True [ { "adviser": {"id": 1}, }, ] then: the response returns a validation error as no assignee can be deleted. """ order = OrderPaidFactory(assignees=[]) assignee = OrderAssigneeFactory(order=order) url = reverse( 'api-v3:omis:order:assignee', kwargs={'order_pk': order.id}, ) response = self.api_client.patch( f'{url}?{AssigneeView.FORCE_DELETE_PARAM}=1', [ { 'adviser': { 'id': assignee.adviser.id }, }, ], ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == { 'non_field_errors': [ 'You cannot delete any assignees at this stage.', ], }
def test_ok_if_assignee_added(self): """ Test that assignees can be added. Given an order with the following assignees: [ { "adviser": {"id": 1}, "estimated_time": 100, "is_lead": true }, { "adviser": {"id": 2}, "estimated_time": 250, "is_lead": false }, ] if I pass the following data: [ { "adviser": {"id": 3}, "actual_time": 100 }, ] then: the adviser is added. """ order = OrderPaidFactory() new_adviser = AdviserFactory() url = reverse( 'api-v3:omis:order:assignee', kwargs={'order_pk': order.id}, ) response = self.api_client.patch( url, [ { 'adviser': { 'id': new_adviser.id }, 'actual_time': 100, }, ], ) assert response.status_code == status.HTTP_200_OK assert str(new_adviser.id) in [ item['adviser']['id'] for item in response.json() ]
def test_403_if_scope_not_allowed(self, scope): """Test that other oauth2 scopes are not allowed.""" order = OrderPaidFactory() url = reverse( 'api-v3:omis-public:payment:collection', kwargs={'public_token': order.public_token}, ) client = self.create_api_client( scope=scope, grant_type=Application.GRANT_CLIENT_CREDENTIALS, ) response = client.get(url) assert response.status_code == status.HTTP_403_FORBIDDEN
def test_verbs_not_allowed(self, verb): """Test that makes sure the other verbs are not allowed.""" order = OrderPaidFactory() url = reverse( 'api-v3:omis-public:payment:collection', kwargs={'public_token': order.public_token}, ) client = self.create_api_client( scope=Scope.public_omis_front_end, grant_type=Application.GRANT_CLIENT_CREDENTIALS, ) response = getattr(client, verb)(url) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
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
def test_ok_if_order_in_allowed_status(self, allowed_status): """ Test that the order can be marked as complete if it's in one of the allowed statuses. """ order = OrderPaidFactory(status=allowed_status, assignees=[]) OrderAssigneeCompleteFactory(order=order) adviser = AdviserFactory() with freeze_time('2018-07-12 13:00'): order.complete(by=adviser) order.refresh_from_db() assert order.status == OrderStatus.complete assert order.completed_on == dateutil_parse('2018-07-12T13:00Z') assert order.completed_by == adviser
def test_atomicity(self): """ Test that if there's a problem with saving the order, nothing gets saved. """ order = OrderPaidFactory(assignees=[]) OrderAssigneeCompleteFactory(order=order) with mock.patch.object(order, 'save') as mocked_save: mocked_save.side_effect = Exception() with pytest.raises(Exception): order.complete(by=None) order.refresh_from_db() assert order.status == OrderStatus.paid assert not order.completed_on assert not order.completed_by
def test_set_actual_time(self): """ Test that actual_time for any assignee can be set. Given an order with the following assignees: [ { "adviser": {"id": 1}, "estimated_time": 100, "is_lead": true }, { "adviser": {"id": 2}, "estimated_time": 250, "is_lead": false }, ] if I pass the following data: [ { "adviser": {"id": 2}, "actual_time": 220 }, ] then: adviser 2 gets updated """ order = OrderPaidFactory(assignees=[]) assignee1 = OrderAssigneeFactory(order=order, estimated_time=100, is_lead=True) assignee2 = OrderAssigneeFactory(order=order, estimated_time=250, is_lead=False) url = reverse( 'api-v3:omis:order:assignee', kwargs={'order_pk': order.id}, ) response = self.api_client.patch( url, [ { 'adviser': { 'id': assignee2.adviser.id }, 'actual_time': 220, }, ], ) assert response.status_code == status.HTTP_200_OK assert response.json() == [ { 'adviser': { 'id': str(assignee1.adviser.id), 'first_name': assignee1.adviser.first_name, 'last_name': assignee1.adviser.last_name, 'name': assignee1.adviser.name, }, 'estimated_time': assignee1.estimated_time, 'actual_time': None, 'is_lead': assignee1.is_lead, }, { 'adviser': { 'id': str(assignee2.adviser.id), 'first_name': assignee2.adviser.first_name, 'last_name': assignee2.adviser.last_name, 'name': assignee2.adviser.name, }, 'estimated_time': assignee2.estimated_time, 'actual_time': 220, 'is_lead': assignee2.is_lead, }, ]
def test_export( self, es_with_collector, request_sortby, orm_ordering, ): """Test export of interaction search results.""" factories = ( OrderCancelledFactory, OrderCompleteFactory, OrderFactory, OrderPaidFactory, OrderSubscriberFactory, OrderWithAcceptedQuoteFactory, OrderWithCancelledQuoteFactory, OrderWithOpenQuoteFactory, OrderWithoutAssigneesFactory, OrderWithoutLeadAssigneeFactory, ApprovedRefundFactory, RequestedRefundFactory, ) order_with_multiple_refunds = OrderPaidFactory() ApprovedRefundFactory( order=order_with_multiple_refunds, requested_amount=order_with_multiple_refunds.total_cost / 5, ) ApprovedRefundFactory( order=order_with_multiple_refunds, requested_amount=order_with_multiple_refunds.total_cost / 4, ) ApprovedRefundFactory( order=order_with_multiple_refunds, requested_amount=order_with_multiple_refunds.total_cost / 3, ) for factory_ in factories: factory_() es_with_collector.flush_and_refresh() data = {} if request_sortby: data['sortby'] = request_sortby url = reverse('api-v3:search:order-export') with freeze_time('2018-01-01 11:12:13'): response = self.api_client.post(url, data=data) assert response.status_code == status.HTTP_200_OK assert parse_header(response.get('Content-Type')) == ('text/csv', { 'charset': 'utf-8' }) assert parse_header(response.get('Content-Disposition')) == ( 'attachment', { 'filename': 'Data Hub - Orders - 2018-01-01-11-12-13.csv' }, ) sorted_orders = Order.objects.order_by(orm_ordering, 'pk') reader = DictReader(StringIO(response.getvalue().decode('utf-8-sig'))) assert reader.fieldnames == list( SearchOrderExportAPIView.field_titles.values()) sorted_orders_and_refunds = ((order, order.refunds.filter( status=RefundStatus.approved)) for order in sorted_orders) expected_row_data = [{ 'Order reference': order.reference, 'Net price': Decimal(order.subtotal_cost) / 100, 'Net refund': Decimal(sum(refund.net_amount for refund in refunds), ) / 100 if refunds else None, 'Status': order.get_status_display(), 'Link': order.get_datahub_frontend_url(), 'Sector': order.sector.name, 'Market': order.primary_market.name, 'UK region': order.uk_region.name, 'Company': order.company.name, 'Company country': order.company.address_country.name, 'Company UK region': get_attr_or_none(order, 'company.uk_region.name'), 'Company link': f'{settings.DATAHUB_FRONTEND_URL_PREFIXES["company"]}' f'/{order.company.pk}', 'Contact': order.contact.name, 'Contact job title': order.contact.job_title, 'Contact link': f'{settings.DATAHUB_FRONTEND_URL_PREFIXES["contact"]}' f'/{order.contact.pk}', 'Lead adviser': get_attr_or_none(order.get_lead_assignee(), 'adviser.name'), 'Created by team': get_attr_or_none(order, 'created_by.dit_team.name'), 'Date created': order.created_on, 'Delivery date': order.delivery_date, 'Date quote sent': get_attr_or_none(order, 'quote.created_on'), 'Date quote accepted': get_attr_or_none(order, 'quote.accepted_on'), 'Date payment received': order.paid_on, 'Date completed': order.completed_on, } for order, refunds in sorted_orders_and_refunds] assert list(dict(row) for row in reader) == format_csv_data(expected_row_data)
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
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.', ], }