Beispiel #1
0
    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)
Beispiel #3
0
    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.',
                ],
            },
        ]
Beispiel #6
0
    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()]
Beispiel #7
0
    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
Beispiel #9
0
    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() == []
Beispiel #10
0
    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)
Beispiel #11
0
    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()
Beispiel #12
0
    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)
Beispiel #13
0
    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()
        ]
Beispiel #16
0
    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
Beispiel #17
0
    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
Beispiel #18
0
    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)
Beispiel #23
0
    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
Beispiel #24
0
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.',
                ],
            }