From 41eda72b2eb08336714d38ade5d3d9b101e8f030 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 5 Jul 2022 19:58:13 +0200
Subject: [PATCH 2/2] pricing: test tool for agenda pricing (#66892)

---
 lingo/agendas/chrono.py                       |  29 ++
 lingo/manager/static/css/style.scss           |   5 +
 lingo/pricing/forms.py                        | 111 +++++++-
 lingo/pricing/models.py                       |  12 +-
 .../manager_agenda_pricing_detail.html        |  25 +-
 lingo/pricing/views.py                        |  10 +
 tests/agendas/test_chrono.py                  | 125 ++++++++-
 tests/pricing/manager/test_agenda.py          | 252 +++++++++++++++++-
 8 files changed, 560 insertions(+), 9 deletions(-)

diff --git a/lingo/agendas/chrono.py b/lingo/agendas/chrono.py
index 3dc8929..c2ed78b 100644
--- a/lingo/agendas/chrono.py
+++ b/lingo/agendas/chrono.py
@@ -17,12 +17,17 @@
 import json
 
 from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
 from requests.exceptions import RequestException
 
 from lingo.agendas.models import Agenda
 from lingo.utils import requests
 
 
+class ChronoError(Exception):
+    pass
+
+
 def is_chrono_enabled():
     return hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('chrono')
 
@@ -103,3 +108,27 @@ def refresh_agendas():
     for slug, agenda in existing_agendas.items():
         if slug not in seen_agendas:
             agenda.delete()
+
+
+def get_event(event_slug):
+    result = get_chrono_json('api/agendas/events/?slots=%s' % event_slug)
+    if not result:
+        raise ChronoError(_('Unable to get event details'))
+    if result.get('err'):
+        raise ChronoError(_('Unable to get event details (%s)') % result['err_desc'])
+    if not result.get('data'):
+        raise ChronoError(_('Unable to get event details'))
+    return result['data'][0]
+
+
+def get_subscriptions(agenda_slug, user_external_id):
+    result = get_chrono_json(
+        'api/agenda/%s/subscription/?user_external_id=%s' % (agenda_slug, user_external_id)
+    )
+    if not result or not result.get('data'):
+        raise ChronoError(_('Unable to get subscription details'))
+    if result.get('err'):
+        raise ChronoError(_('Unable to get subscription details (%s)') % result['err_desc'])
+    if not result.get('data'):
+        raise ChronoError(_('Unable to get subscription details'))
+    return result['data']
diff --git a/lingo/manager/static/css/style.scss b/lingo/manager/static/css/style.scss
index 7cfd025..1fb43fc 100644
--- a/lingo/manager/static/css/style.scss
+++ b/lingo/manager/static/css/style.scss
@@ -47,3 +47,8 @@ ul.objects-list.sortable {
 ul.objects-list.single-links li a.link::before {
 	content: "\f08e"; /* fa-external-link  */
 }
+
+div.test-tool-result .infonotice h3 {
+	margin-top: 0;
+	font-weight: normal;
+}
diff --git a/lingo/pricing/forms.py b/lingo/pricing/forms.py
index 4795993..c35e5b7 100644
--- a/lingo/pricing/forms.py
+++ b/lingo/pricing/forms.py
@@ -14,14 +14,17 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import datetime
+
 from django import forms
 from django.forms import ValidationError
 from django.template import Template, TemplateSyntaxError
 from django.utils.timezone import now
 from django.utils.translation import ugettext_lazy as _
 
+from lingo.agendas.chrono import ChronoError, get_event, get_subscriptions
 from lingo.agendas.models import CheckType
-from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory
+from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, PricingError
 
 
 class ExportForm(forms.Form):
@@ -162,6 +165,112 @@ class PricingMatrixForm(forms.Form):
             self.fields['crit_%i' % i] = forms.DecimalField(required=True, max_digits=5, decimal_places=2)
 
 
+class PricingTestToolForm(forms.Form):
+    event_slug = forms.CharField(label=_('Event identifier'))
+    user_external_id = forms.CharField(label=_('User external identifier'))
+    adult_external_id = forms.CharField(label=_('Adult external identifier'))
+    booking_status = forms.ChoiceField(label=_('Booking status'), choices=[])
+
+    def __init__(self, *args, **kwargs):
+        self.request = kwargs.pop('request')
+        self.agenda_pricing = kwargs.pop('agenda_pricing')
+        self.agenda = self.agenda_pricing.agenda
+        self.serialized_event = None
+        self.serialized_subscription = None
+        self.check_type_slug = None
+        self.booking_status = None
+        super().__init__(*args, **kwargs)
+        presence_check_types = (
+            self.agenda.check_type_group.check_types.presences() if self.agenda.check_type_group else []
+        )
+        absence_check_types = (
+            self.agenda.check_type_group.check_types.absences() if self.agenda.check_type_group else []
+        )
+        status_choices = [
+            ('presence', _('Presence')),
+        ]
+        status_choices += [
+            ('presence::%s' % ct.slug, _('Presence (%s)') % ct.label) for ct in presence_check_types
+        ]
+        status_choices += [('absence', _('Absence'))]
+        status_choices += [
+            ('absence::%s' % ct.slug, _('Absence (%s)') % ct.label) for ct in absence_check_types
+        ]
+        self.fields['booking_status'].choices = status_choices
+
+    def clean_event_slug(self):
+        original_event_slug = self.cleaned_data['event_slug']
+        event_slug = original_event_slug
+        if '@' not in event_slug:
+            # chrono's endpoint takes event ids like '<agenda_slug>@<event_slug>'
+            event_slug = '%s@%s' % (self.agenda.slug, event_slug)
+        elif event_slug.split('@')[0] != self.agenda.slug:
+            raise ValidationError(_('The agenda identifier is wrong (%s)') % event_slug.split('@')[0])
+        try:
+            self.serialized_event = get_event(event_slug)
+        except ChronoError as e:
+            raise forms.ValidationError(str(e).replace(event_slug, original_event_slug))
+
+        event_date = datetime.datetime.fromisoformat(self.serialized_event['start_datetime']).date()
+        if event_date < self.agenda_pricing.date_start or event_date >= self.agenda_pricing.date_end:
+            raise ValidationError(_('This event takes place outside the period covered by this pricing'))
+
+        return original_event_slug
+
+    def clean_booking_status(self):
+        original_booking_status = self.cleaned_data['booking_status']
+        self.booking_status = original_booking_status
+        if '::' in original_booking_status:
+            # split value to get booking status and selected check_type
+            self.booking_status, self.check_type_slug = original_booking_status.split('::')
+        return original_booking_status
+
+    def get_subscription(self, user_external_id):
+        start_date = datetime.datetime.fromisoformat(self.serialized_event['start_datetime']).date()
+        end_date = start_date + datetime.timedelta(days=1)
+        try:
+            subscriptions = get_subscriptions(self.agenda.slug, user_external_id)
+        except ChronoError as e:
+            self.add_error('user_external_id', str(e))
+            return
+        for subscription in subscriptions:
+            sub_start_date = datetime.date.fromisoformat(subscription['date_start'])
+            sub_end_date = datetime.date.fromisoformat(subscription['date_end'])
+            if sub_start_date >= end_date:
+                continue
+            if sub_end_date <= start_date:
+                continue
+            return subscription
+        self.add_error('user_external_id', _('No subscription found for this event'))
+
+    def clean(self):
+        super().clean()
+        if self.cleaned_data.get('user_external_id') and self.serialized_event:
+            user_external_id = self.cleaned_data['user_external_id']
+            self.serialized_subscription = self.get_subscription(user_external_id)
+
+    def compute(self):
+        if not self.serialized_event or not self.serialized_subscription:
+            return
+        try:
+            return AgendaPricing.get_pricing_data(
+                request=self.request,
+                agenda=self.agenda,
+                agenda_pricing=self.agenda_pricing,
+                event=self.serialized_event,
+                subscription=self.serialized_subscription,
+                check_status={
+                    'status': self.booking_status,
+                    'check_type': self.check_type_slug,
+                },
+                booking={},
+                user_external_id=self.cleaned_data['user_external_id'],
+                adult_external_id=self.cleaned_data['adult_external_id'],
+            )
+        except PricingError as e:
+            return {'error': e.details}
+
+
 class NewCheckTypeForm(forms.ModelForm):
     class Meta:
         model = CheckType
diff --git a/lingo/pricing/models.py b/lingo/pricing/models.py
index 3329668..8e42436 100644
--- a/lingo/pricing/models.py
+++ b/lingo/pricing/models.py
@@ -363,9 +363,17 @@ class AgendaPricing(models.Model):
 
     @staticmethod
     def get_pricing_data(
-        request, agenda, event, subscription, check_status, booking, user_external_id, adult_external_id
+        request,
+        agenda,
+        event,
+        subscription,
+        check_status,
+        booking,
+        user_external_id,
+        adult_external_id,
+        agenda_pricing=None,
     ):
-        agenda_pricing = AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
+        agenda_pricing = agenda_pricing or AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
         data = {
             'event': event,
             'subscription': subscription,
diff --git a/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
index 866f63c..fe6afd5 100644
--- a/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
+++ b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
@@ -20,8 +20,31 @@
 {% endblock %}
 
 {% block content %}
-{% for matrix in object.iter_pricing_matrix %}
 <div class="section">
+  <h3>{% trans "Test tool" %}</h3>
+<div>
+  <form method="get" enctype="multipart/form-data">
+    {{ test_tool_form.as_p }}
+    <div class="buttons">
+      <button class="submit-button">{% trans "Compute" %}</button>
+    </div>
+  </form>
+  {% if request.GET and test_tool_form.is_valid %}
+  {% with test_tool_form.compute as pricing_data %}
+  <div class="test-tool-result">
+    <div class="infonotice">
+        <h3>{% trans "Computed pricing data" %}</h3>
+        {% if pricing_data.pricing is not None %}<p>{% trans "Pricing:" %} {{ pricing_data.pricing|stringformat:".2f" }}</p>{% endif %}
+        <pre>{{ pricing_data|pprint }}</pre>
+    </div>
+  </div>
+  {% endwith %}
+  {% endif %}
+</div>
+</div>
+
+{% for matrix in object.iter_pricing_matrix %}
+<div class="section pricing-matrix">
   {% if matrix.criteria %}<h3>{{ matrix.criteria.label }}</h3>{% endif %}
 <div>
   <table class="main pricing-matrix-{{ matrix.criteria.slug }}">
diff --git a/lingo/pricing/views.py b/lingo/pricing/views.py
index 366843a..242984f 100644
--- a/lingo/pricing/views.py
+++ b/lingo/pricing/views.py
@@ -54,6 +54,7 @@ from lingo.pricing.forms import (
     PricingCriteriaCategoryEditForm,
     PricingDuplicateForm,
     PricingMatrixForm,
+    PricingTestToolForm,
     PricingVariableFormSet,
 )
 from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
@@ -717,6 +718,15 @@ class AgendaPricingDetailView(AgendaMixin, DetailView):
             'pricing__criterias__category'
         )
 
+    def get_context_data(self, **kwargs):
+        form = PricingTestToolForm(
+            agenda_pricing=self.object, request=self.request, data=self.request.GET or None
+        )
+        if self.request.GET:
+            form.is_valid()
+        kwargs['test_tool_form'] = form
+        return super().get_context_data(**kwargs)
+
 
 agenda_pricing_detail = AgendaPricingDetailView.as_view()
 
diff --git a/tests/agendas/test_chrono.py b/tests/agendas/test_chrono.py
index 5d06579..33cb47e 100644
--- a/tests/agendas/test_chrono.py
+++ b/tests/agendas/test_chrono.py
@@ -5,7 +5,13 @@ import pytest
 from requests.exceptions import ConnectionError
 from requests.models import Response
 
-from lingo.agendas.chrono import collect_agenda_data, refresh_agendas
+from lingo.agendas.chrono import (
+    ChronoError,
+    collect_agenda_data,
+    get_event,
+    get_subscriptions,
+    refresh_agendas,
+)
 from lingo.agendas.models import Agenda
 
 pytestmark = pytest.mark.django_db
@@ -151,3 +157,120 @@ def test_refresh_agendas(mock_collect):
     mock_collect.return_value = []
     refresh_agendas()
     assert Agenda.objects.count() == 0
+
+
+def test_get_event_no_service(settings):
+    settings.KNOWN_SERVICES = {}
+    with pytest.raises(ChronoError) as e:
+        get_event('foo')
+    assert str(e.value) == 'Unable to get event details'
+
+    settings.KNOWN_SERVICES = {'other': []}
+    with pytest.raises(ChronoError) as e:
+        get_event('foo')
+    assert str(e.value) == 'Unable to get event details'
+
+
+def test_get_event():
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.side_effect = ConnectionError()
+        with pytest.raises(ChronoError) as e:
+            get_event('foo')
+        assert str(e.value) == 'Unable to get event details'
+
+    with mock.patch('requests.Session.get') as requests_get:
+        mock_resp = Response()
+        mock_resp.status_code = 500
+        requests_get.return_value = mock_resp
+        with pytest.raises(ChronoError) as e:
+            get_event('foo')
+        assert str(e.value) == 'Unable to get event details'
+
+    with mock.patch('requests.Session.get') as requests_get:
+        mock_resp = Response()
+        mock_resp.status_code = 404
+        requests_get.return_value = mock_resp
+        with pytest.raises(ChronoError) as e:
+            get_event('foo')
+        assert str(e.value) == 'Unable to get event details'
+
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.return_value = MockedRequestResponse(content=json.dumps({'foo': 'bar'}))
+        with pytest.raises(ChronoError) as e:
+            get_event('foo')
+        assert str(e.value) == 'Unable to get event details'
+
+    data = {'data': []}
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
+        with pytest.raises(ChronoError) as e:
+            get_event('foo')
+        assert str(e.value) == 'Unable to get event details'
+        assert requests_get.call_args_list[0][0] == ('api/agendas/events/?slots=foo',)
+        assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://chrono.example.org'
+
+    data = {'data': ['foo']}
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
+        assert get_event('foo') == 'foo'
+
+    data = {'data': ['foo', 'bar']}  # should not happen
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
+        assert get_event('foo') == 'foo'
+
+
+def test_get_subscriptions_no_service(settings):
+    settings.KNOWN_SERVICES = {}
+    with pytest.raises(ChronoError) as e:
+        get_subscriptions('foo', 'user:1')
+    assert str(e.value) == 'Unable to get subscription details'
+
+    settings.KNOWN_SERVICES = {'other': []}
+    with pytest.raises(ChronoError) as e:
+        get_subscriptions('foo', 'user:1')
+    assert str(e.value) == 'Unable to get subscription details'
+
+
+def test_get_subscriptions():
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.side_effect = ConnectionError()
+        with pytest.raises(ChronoError) as e:
+            get_subscriptions('foo', 'user:1')
+        assert str(e.value) == 'Unable to get subscription details'
+
+    with mock.patch('requests.Session.get') as requests_get:
+        mock_resp = Response()
+        mock_resp.status_code = 500
+        requests_get.return_value = mock_resp
+        with pytest.raises(ChronoError) as e:
+            get_subscriptions('foo', 'user:1')
+        assert str(e.value) == 'Unable to get subscription details'
+
+    with mock.patch('requests.Session.get') as requests_get:
+        mock_resp = Response()
+        mock_resp.status_code = 404
+        requests_get.return_value = mock_resp
+        with pytest.raises(ChronoError) as e:
+            get_subscriptions('foo', 'user:1')
+        assert str(e.value) == 'Unable to get subscription details'
+
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.return_value = MockedRequestResponse(content=json.dumps({'foo': 'bar'}))
+        with pytest.raises(ChronoError) as e:
+            get_subscriptions('foo', 'user:1')
+        assert str(e.value) == 'Unable to get subscription details'
+
+    data = {'data': []}
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
+        with pytest.raises(ChronoError) as e:
+            get_subscriptions('foo', 'user:1')
+        assert str(e.value) == 'Unable to get subscription details'
+        assert requests_get.call_args_list[0][0] == ('api/agenda/foo/subscription/?user_external_id=user:1',)
+        assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://chrono.example.org'
+
+    data = {'data': ['foo', 'bar']}
+    with mock.patch('requests.Session.get') as requests_get:
+        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
+        assert get_subscriptions('foo', 'user:1') == ['foo', 'bar']
diff --git a/tests/pricing/manager/test_agenda.py b/tests/pricing/manager/test_agenda.py
index 571e518..106fe1d 100644
--- a/tests/pricing/manager/test_agenda.py
+++ b/tests/pricing/manager/test_agenda.py
@@ -1,11 +1,13 @@
 import datetime
+from decimal import Decimal
 from unittest import mock
 
 import pytest
 from django.utils.timezone import now
 
-from lingo.agendas.models import Agenda, CheckTypeGroup
-from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing
+from lingo.agendas.chrono import ChronoError
+from lingo.agendas.models import Agenda, CheckType, CheckTypeGroup
+from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingError
 from tests.utils import login
 
 pytestmark = pytest.mark.django_db
@@ -353,7 +355,7 @@ def test_detail_agenda_pricing_2_categories(app, admin_user):
 
     app = login(app)
     resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
-    assert '<h3>' not in resp
+    assert len(resp.pyquery.find('div.section.prixing-matrix h3')) == 0
     ths = resp.pyquery.find('table thead th')
     assert len(ths) == 4
     assert ths[0].text is None
@@ -400,7 +402,7 @@ def test_detail_agenda_pricing_1_category(app, admin_user):
 
     app = login(app)
     resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
-    assert '<h3>' not in resp
+    assert len(resp.pyquery.find('div.section.prixing-matrix h3')) == 0
     ths = resp.pyquery.find('table thead')
     assert len(ths) == 0
     assert resp.pyquery.find('table tr.pricing-row-crit-3-1 th')[0].text == 'Crit 3-1'
@@ -413,6 +415,248 @@ def test_detail_agenda_pricing_1_category(app, admin_user):
     assert resp.pyquery.find('table tr.pricing-row-crit-3-4 td')[0].text == '114.00'
 
 
+@mock.patch('lingo.pricing.forms.get_event')
+@mock.patch('lingo.pricing.forms.get_subscriptions')
+@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data')
+def test_detail_agenda_pricing_test_tool(mock_pricing_data, mock_subscriptions, mock_event, app, admin_user):
+    agenda = Agenda.objects.create(label='Foo bar')
+    pricing = Pricing.objects.create(label='Foo bar')
+    agenda_pricing = AgendaPricing.objects.create(
+        agenda=agenda,
+        pricing=pricing,
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=10, day=1),
+    )
+
+    app = login(app)
+    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
+    assert 'Computed pricing data' not in resp
+
+    # check event date
+    mock_event.return_value = {'start_datetime': '2021-08-31T12:00:00+02:00'}
+    resp.form['event_slug'] = 'foo'
+    resp.form['user_external_id'] = 'user:1'
+    resp.form['adult_external_id'] = 'adult:1'
+    resp.form['booking_status'] = 'presence'
+    resp = resp.form.submit()
+    assert 'Computed pricing data' not in resp
+    assert resp.context['test_tool_form'].errors['event_slug'] == [
+        'This event takes place outside the period covered by this pricing'
+    ]
+    assert mock_event.call_args_list == [mock.call('foo-bar@foo')]
+    assert mock_pricing_data.call_args_list == []
+    mock_event.return_value = {'start_datetime': '2021-10-01T12:00:00+02:00'}
+    resp = resp.form.submit()
+    assert 'Computed pricing data' not in resp
+    assert resp.context['test_tool_form'].errors['event_slug'] == [
+        'This event takes place outside the period covered by this pricing'
+    ]
+
+    mock_event.return_value = {'start_datetime': '2021-09-01T12:00:00+02:00'}
+
+    # check event_slug & agenda
+    resp.form['event_slug'] = 'foo@foo'
+    resp = resp.form.submit()
+    assert resp.context['test_tool_form'].errors['event_slug'] == ['The agenda identifier is wrong (foo)']
+
+    # check subscriptions dates
+    mock_subscriptions.return_value = []
+    mock_event.reset_mock()
+    resp.form['event_slug'] = 'foo-bar@foo'
+    resp = resp.form.submit()
+    assert mock_event.call_args_list == [mock.call('foo-bar@foo')]
+    assert mock_pricing_data.call_args_list == []
+    assert 'Computed pricing data' not in resp
+    assert resp.context['test_tool_form'].errors['user_external_id'] == [
+        'No subscription found for this event'
+    ]
+
+    mock_subscriptions.return_value = [
+        {
+            'date_start': '2021-08-01',
+            'date_end': '2021-09-01',
+        },
+        {
+            'date_start': '2021-09-02',
+            'date_end': '2021-09-03',
+        },
+    ]
+    resp = resp.form.submit()
+    assert 'Computed pricing data' not in resp
+    assert resp.context['test_tool_form'].errors['user_external_id'] == [
+        'No subscription found for this event'
+    ]
+
+    mock_subscriptions.return_value = [
+        {
+            'date_start': '2021-08-01',
+            'date_end': '2021-09-01',
+        },
+        {
+            'date_start': '2021-09-01',
+            'date_end': '2021-09-02',
+        },
+        {
+            'date_start': '2021-09-02',
+            'date_end': '2021-09-03',
+        },
+    ]
+    mock_pricing_data.return_value = {'foo': 'bar', 'pricing': Decimal('42')}
+    resp = resp.form.submit()
+    assert 'Computed pricing data' in resp
+    assert mock_pricing_data.call_args_list == [
+        mock.call(
+            request=mock.ANY,
+            agenda=agenda,
+            agenda_pricing=agenda_pricing,
+            event={'start_datetime': '2021-09-01T12:00:00+02:00'},
+            subscription={'date_start': '2021-09-01', 'date_end': '2021-09-02'},
+            check_status={'status': 'presence', 'check_type': None},
+            booking={},
+            user_external_id='user:1',
+            adult_external_id='adult:1',
+        )
+    ]
+    assert '<p>Pricing: 42.00</p>' in resp
+    assert '<pre>{&#39;foo&#39;: &#39;bar&#39;, &#39;pricing&#39;: Decimal(&#39;42&#39;)}</pre>' in resp
+
+    mock_pricing_data.side_effect = PricingError(details={'foo': 'error'})
+    resp = resp.form.submit()
+    assert 'Computed pricing data' in resp
+    assert '<pre>{&#39;error&#39;: {&#39;foo&#39;: &#39;error&#39;}}</pre>' in resp
+
+
+@mock.patch('lingo.pricing.forms.get_event')
+@mock.patch('lingo.pricing.forms.get_subscriptions')
+def test_detail_agenda_pricing_test_tool_event_error(mock_subscriptions, mock_event, app, admin_user):
+    agenda = Agenda.objects.create(label='Foo bar')
+    pricing = Pricing.objects.create(label='Foo bar')
+    agenda_pricing = AgendaPricing.objects.create(
+        agenda=agenda,
+        pricing=pricing,
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=10, day=1),
+    )
+
+    mock_event.side_effect = ChronoError('foo bar foo-bar@foo-event')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
+    resp.form['event_slug'] = 'foo-event'
+    resp.form['user_external_id'] = 'user:1'
+    resp.form['adult_external_id'] = 'adult:1'
+    resp.form['booking_status'] = 'presence'
+
+    # agenda slug is removed from error message
+    resp = resp.form.submit()
+    assert resp.context['test_tool_form'].errors['event_slug'] == ['foo bar foo-event']
+
+    # except it was included in event_slug
+    resp.form['event_slug'] = 'foo-bar@foo-event'
+    resp = resp.form.submit()
+    assert resp.context['test_tool_form'].errors['event_slug'] == ['foo bar foo-bar@foo-event']
+
+
+@mock.patch('lingo.pricing.forms.get_event')
+@mock.patch('lingo.pricing.forms.get_subscriptions')
+def test_detail_agenda_pricing_test_tool_subscription_error(mock_subscriptions, mock_event, app, admin_user):
+    agenda = Agenda.objects.create(label='Foo bar')
+    pricing = Pricing.objects.create(label='Foo bar')
+    agenda_pricing = AgendaPricing.objects.create(
+        agenda=agenda,
+        pricing=pricing,
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=10, day=1),
+    )
+
+    mock_event.return_value = {'start_datetime': '2021-09-01T12:00:00+02:00'}
+    mock_subscriptions.side_effect = ChronoError('foo bar')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
+    resp.form['event_slug'] = 'foo-event'
+    resp.form['user_external_id'] = 'user:1'
+    resp.form['adult_external_id'] = 'adult:1'
+    resp.form['booking_status'] = 'presence'
+    resp = resp.form.submit()
+    assert resp.context['test_tool_form'].errors['user_external_id'] == ['foo bar']
+
+
+@mock.patch('lingo.pricing.forms.get_event')
+@mock.patch('lingo.pricing.forms.get_subscriptions')
+@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data')
+def test_detail_agenda_pricing_test_tool_booking_status(
+    mock_pricing_data, mock_subscriptions, mock_event, app, admin_user
+):
+    agenda = Agenda.objects.create(label='Foo bar')
+    pricing = Pricing.objects.create(label='Foo bar')
+    agenda_pricing = AgendaPricing.objects.create(
+        agenda=agenda,
+        pricing=pricing,
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=10, day=1),
+    )
+
+    mock_event.return_value = {'start_datetime': '2021-09-01T12:00:00+02:00'}
+    mock_subscriptions.return_value = [
+        {
+            'date_start': '2021-09-01',
+            'date_end': '2021-09-02',
+        },
+    ]
+
+    app = login(app)
+    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
+    assert resp.form['booking_status'].options == [
+        ('presence', False, 'Presence'),
+        ('absence', False, 'Absence'),
+    ]
+
+    group = CheckTypeGroup.objects.create(label='Foo bar')
+    CheckType.objects.create(label='Foo presence reason', group=group, kind='presence')
+    CheckType.objects.create(label='Foo absence reason', group=group, kind='absence')
+    agenda.check_type_group = group
+    agenda.save()
+
+    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
+    assert resp.form['booking_status'].options == [
+        ('presence', False, 'Presence'),
+        ('presence::foo-presence-reason', False, 'Presence (Foo presence reason)'),
+        ('absence', False, 'Absence'),
+        ('absence::foo-absence-reason', False, 'Absence (Foo absence reason)'),
+    ]
+    resp.form['event_slug'] = 'foo'
+    resp.form['user_external_id'] = 'user:1'
+    resp.form['adult_external_id'] = 'adult:1'
+    resp.form['booking_status'] = 'presence'
+    resp = resp.form.submit()
+    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
+        'check_type': None,
+        'status': 'presence',
+    }
+
+    mock_pricing_data.reset_mock()
+    resp.form['booking_status'] = 'presence::foo-presence-reason'
+    resp = resp.form.submit()
+    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
+        'check_type': 'foo-presence-reason',
+        'status': 'presence',
+    }
+
+    mock_pricing_data.reset_mock()
+    resp.form['booking_status'] = 'absence'
+    resp = resp.form.submit()
+    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {'check_type': None, 'status': 'absence'}
+
+    mock_pricing_data.reset_mock()
+    resp.form['booking_status'] = 'absence::foo-absence-reason'
+    resp = resp.form.submit()
+    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
+        'check_type': 'foo-absence-reason',
+        'status': 'absence',
+    }
+
+
 def test_edit_agenda_pricing_matrix_3_categories(app, admin_user):
     category1 = CriteriaCategory.objects.create(label='Cat 1')
     criteria11 = Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
-- 
2.35.1

