From e1ee74906de5139842d803914086e6481bfa8f12 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 28 Jul 2022 16:24:13 +0200
Subject: [PATCH 11/14] pricing: adapt test tool for flat fee schedule mode
 (#67675)

---
 lingo/pricing/forms.py                        | 115 ++++++---
 lingo/pricing/models.py                       |   3 +
 .../manager_agenda_pricing_detail.html        |   4 +-
 tests/pricing/manager/test_agenda_pricing.py  | 223 +++++++++++++++++-
 4 files changed, 310 insertions(+), 35 deletions(-)

diff --git a/lingo/pricing/forms.py b/lingo/pricing/forms.py
index b6ec1ed..ca8dbe5 100644
--- a/lingo/pricing/forms.py
+++ b/lingo/pricing/forms.py
@@ -289,6 +289,9 @@ class PricingMatrixForm(forms.Form):
 class PricingTestToolForm(forms.Form):
     agenda = forms.ModelChoiceField(label=_('Agenda'), empty_label=None, queryset=Agenda.objects.none())
     event_slug = forms.CharField(label=_('Event identifier'))
+    billing_date = forms.ModelChoiceField(
+        label=_('Billing date'), empty_label=None, queryset=BillingDate.objects.none()
+    )
     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=[])
@@ -304,7 +307,25 @@ class PricingTestToolForm(forms.Form):
         self.check_type_slug = None
         self.booking_status = None
         super().__init__(*args, **kwargs)
-        self.fields['agenda'].queryset = self.agenda_pricing.agendas.all()
+        if self.agenda_pricing.subscription_required:
+            self.fields['agenda'].queryset = self.agenda_pricing.agendas.all()
+        else:
+            del self.fields['agenda']
+        if self.agenda_pricing.flat_fee_schedule:
+            del self.fields['event_slug']
+            del self.fields['booking_status']
+            self.init_billing_date()
+        else:
+            del self.fields['billing_date']
+            self.init_booking_status()
+
+    def init_agenda(self, agenda_id):
+        try:
+            self.agenda = self.agenda_pricing.agendas.get(pk=agenda_id)
+        except Agenda.DoesNotExist:
+            pass
+
+    def init_booking_status(self):
         presence_check_types = (
             self.agenda.check_type_group.check_types.presences()
             if self.agenda and self.agenda.check_type_group
@@ -327,11 +348,12 @@ class PricingTestToolForm(forms.Form):
         ]
         self.fields['booking_status'].choices = status_choices
 
-    def init_agenda(self, agenda_id):
-        try:
-            self.agenda = self.agenda_pricing.agendas.get(pk=agenda_id)
-        except Agenda.DoesNotExist:
-            pass
+    def init_billing_date(self):
+        billing_dates = self.agenda_pricing.billingdates.order_by('date_start')
+        if not billing_dates:
+            del self.fields['billing_date']
+            return
+        self.fields['billing_date'].queryset = billing_dates
 
     def clean_event_slug(self):
         event_slug = self.cleaned_data['event_slug']
@@ -356,9 +378,7 @@ class PricingTestToolForm(forms.Form):
             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)
+    def get_subscription(self, user_external_id, start_date, end_date):
         try:
             subscriptions = get_subscriptions(self.agenda.slug, user_external_id)
         except ChronoError as e:
@@ -372,33 +392,70 @@ class PricingTestToolForm(forms.Form):
             if sub_end_date <= start_date:
                 continue
             return subscription
-        self.add_error('user_external_id', _('No subscription found for this event'))
+        error_message = _('No subscription found for this event')
+        if self.agenda_pricing.flat_fee_schedule:
+            error_message = _('No subscription found for this period')
+        self.add_error('user_external_id', error_message)
 
     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)
+
+        user_external_id = self.cleaned_data.get('user_external_id')
+        start_date = None
+        end_date = None
+        if self.agenda_pricing.flat_fee_schedule and self.agenda_pricing.subscription_required:
+            if self.cleaned_data.get('billing_date'):
+                start_date = self.cleaned_data['billing_date'].date_start
+                next_billing_date = (
+                    self.fields['billing_date'].queryset.filter(date_start__gt=start_date).first()
+                )
+                end_date = next_billing_date.date_start if next_billing_date else self.agenda_pricing.date_end
+            else:
+                start_date = self.agenda_pricing.date_start
+                end_date = self.agenda_pricing.date_end
+        elif self.serialized_event:
+            start_date = datetime.datetime.fromisoformat(self.serialized_event['start_datetime']).date()
+            end_date = start_date + datetime.timedelta(days=1)
+        if user_external_id and start_date and end_date:
+            self.serialized_subscription = self.get_subscription(user_external_id, start_date, end_date)
 
     def compute(self):
-        if not self.serialized_event or not self.serialized_subscription:
-            return
         try:
-            return self.agenda_pricing.get_pricing_data_for_event(
-                request=self.request,
-                agenda=self.agenda,
-                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'],
-            )
+            if self.agenda_pricing.flat_fee_schedule:
+                return self.compute_for_flat_fee_schedule()
+            return self.compute_for_event()
         except PricingError as e:
-            return {'error': e.details}
+            return {
+                'error': type(e),
+                'error_details': e.details,
+            }
+
+    def compute_for_flat_fee_schedule(self):
+        if self.agenda_pricing.subscription_required and not self.serialized_subscription:
+            return
+        return self.agenda_pricing.get_pricing_data(
+            request=self.request,
+            subscription=self.serialized_subscription,
+            user_external_id=self.cleaned_data['user_external_id'],
+            adult_external_id=self.cleaned_data['adult_external_id'],
+        )
+
+    def compute_for_event(self):
+        if not self.serialized_event or not self.serialized_subscription:
+            return
+        return self.agenda_pricing.get_pricing_data_for_event(
+            request=self.request,
+            agenda=self.agenda,
+            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'],
+        )
 
 
 class NewCheckTypeForm(forms.ModelForm):
diff --git a/lingo/pricing/models.py b/lingo/pricing/models.py
index 3ef41e3..cb273fe 100644
--- a/lingo/pricing/models.py
+++ b/lingo/pricing/models.py
@@ -694,6 +694,9 @@ class BillingDate(models.Model):
     date_start = models.DateField(_('Billing start date'))
     label = models.CharField(_('Label'), max_length=150)
 
+    def __str__(self):
+        return '%s (%s)' % (self.date_start.strftime('%d/%m/%Y'), self.label)
+
     def export_json(self):
         return {
             'date_start': self.date_start.strftime('%Y-%m-%d'),
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 8cdd647..15af0a2 100644
--- a/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
+++ b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
@@ -74,6 +74,7 @@
     <div aria-labelledby="tab-debug" hidden id="panel-debug" role="tabpanel" tabindex="0">
       <form method="get" enctype="multipart/form-data" action="{% url 'lingo-manager-agenda-pricing-test-tool' object.pk %}">
         {{ test_tool_form.as_p }}
+        {% if not object.flat_fee_schedule %}
         <script>
           $(function() {
             var presences = {};
@@ -109,6 +110,7 @@
             });
           });
         </script>
+        {% endif %}
         <div class="buttons">
           <button class="submit-button">{% trans "Compute" %}</button>
         </div>
@@ -133,7 +135,7 @@
         {% for billing_date in billing_dates %}
         <li>
           <a rel="popup" href="{% url 'lingo-manager-agenda-pricing-billing-date-edit' object.pk billing_date.pk %}">
-            {{ billing_date.date_start|date:'d/m/Y' }} ({{ billing_date.label }})
+            {{ billing_date }}
           </a>
           <a class="delete" rel="popup" href="{% url 'lingo-manager-agenda-pricing-billing-date-delete' object.pk billing_date.pk %}">{% trans "remove"%}</a>
         </li>
diff --git a/tests/pricing/manager/test_agenda_pricing.py b/tests/pricing/manager/test_agenda_pricing.py
index 0aa62f2..019cfcc 100644
--- a/tests/pricing/manager/test_agenda_pricing.py
+++ b/tests/pricing/manager/test_agenda_pricing.py
@@ -822,7 +822,7 @@ def test_detail_agenda_pricing_1_category(app, admin_user):
 @mock.patch('lingo.pricing.forms.get_event')
 @mock.patch('lingo.pricing.forms.get_subscriptions')
 @mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
-def test_detail_agenda_pricing_test_tool(
+def test_detail_agenda_pricing_test_tool_for_event(
     mock_pricing_data_event, mock_subscriptions, mock_event, app, admin_user
 ):
     agenda = Agenda.objects.create(label='Foo bar')
@@ -836,6 +836,7 @@ def test_detail_agenda_pricing_test_tool(
 
     app = login(app)
     resp = app.get('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
+    assert 'billing_date' not in resp.context['test_tool_form'].fields
     assert 'Computed pricing data' not in resp
 
     # check event date
@@ -921,12 +922,15 @@ def test_detail_agenda_pricing_test_tool(
     mock_pricing_data_event.side_effect = PricingError(details={'foo': 'error'})
     resp = resp.form.submit().follow()
     assert 'Computed pricing data' in resp
-    assert '<pre>{&#39;error&#39;: {&#39;foo&#39;: &#39;error&#39;}}</pre>' in resp
+    assert '&#39;error&#39;: &lt;class &#39;lingo.pricing.models.PricingError&#39;&gt;' in resp
+    assert '&#39;error_details&#39;: {&#39;foo&#39;: &#39;error&#39;}' 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):
+def test_detail_agenda_pricing_test_tool_for_event_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(
@@ -951,7 +955,9 @@ def test_detail_agenda_pricing_test_tool_event_error(mock_subscriptions, mock_ev
 
 @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):
+def test_detail_agenda_pricing_test_tool_for_event_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(
@@ -978,7 +984,7 @@ def test_detail_agenda_pricing_test_tool_subscription_error(mock_subscriptions,
 @mock.patch('lingo.pricing.forms.get_event')
 @mock.patch('lingo.pricing.forms.get_subscriptions')
 @mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
-def test_detail_agenda_pricing_test_tool_booking_status(
+def test_detail_agenda_pricing_test_tool_for_event_booking_status(
     mock_pricing_data_event, mock_subscriptions, mock_event, app, admin_user
 ):
     agenda = Agenda.objects.create(label='Foo bar')
@@ -1058,6 +1064,213 @@ def test_detail_agenda_pricing_test_tool_booking_status(
     }
 
 
+@mock.patch('lingo.pricing.forms.get_subscriptions')
+@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data')
+def test_detail_agenda_pricing_test_tool_for_flat_fee_schedule(
+    mock_pricing_data, mock_subscriptions, app, admin_user
+):
+    agenda = Agenda.objects.create(label='Foo bar')
+    pricing = Pricing.objects.create(label='Foo bar')
+    agenda_pricing = AgendaPricing.objects.create(
+        pricing=pricing,
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=10, day=1),
+        flat_fee_schedule=True,
+        subscription_required=True,
+    )
+    agenda_pricing.agendas.add(agenda)
+
+    app = login(app)
+    resp = app.get('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
+    assert 'event_slug' not in resp.context['test_tool_form'].fields
+    assert 'booking_status' not in resp.context['test_tool_form'].fields
+    assert 'billing_date' not in resp.context['test_tool_form'].fields
+    assert 'Computed pricing data' not in resp
+
+    # check subscriptions dates
+    mock_subscriptions.return_value = []
+    resp.form['agenda'] = agenda.pk
+    resp.form['user_external_id'] = 'user:1'
+    resp.form['adult_external_id'] = 'adult:1'
+    resp = resp.form.submit().follow()
+    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 period'
+    ]
+
+    mock_subscriptions.return_value = [
+        {
+            'date_start': '2021-08-01',
+            'date_end': '2021-09-01',
+        },
+        {
+            'date_start': '2021-10-01',
+            'date_end': '2021-11-01',
+        },
+    ]
+    resp = resp.form.submit().follow()
+    assert 'Computed pricing data' not in resp
+    assert resp.context['test_tool_form'].errors['user_external_id'] == [
+        'No subscription found for this period'
+    ]
+
+    mock_subscriptions.return_value = [
+        {
+            'date_start': '2021-08-01',
+            'date_end': '2021-09-01',
+        },
+        {
+            'date_start': '2021-09-02',
+            'date_end': '2021-09-03',
+        },
+        {
+            'date_start': '2021-10-01',
+            'date_end': '2021-11-01',
+        },
+    ]
+    mock_pricing_data.return_value = {'foo': 'bar', 'pricing': Decimal('42')}
+    resp = resp.form.submit().follow()
+    assert 'Computed pricing data' in resp
+    assert mock_pricing_data.call_args_list == [
+        mock.call(
+            request=mock.ANY,
+            subscription={'date_start': '2021-09-02', 'date_end': '2021-09-03'},
+            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().follow()
+    assert 'Computed pricing data' in resp
+    assert '&#39;error&#39;: &lt;class &#39;lingo.pricing.models.PricingError&#39;&gt;' in resp
+    assert '&#39;error_details&#39;: {&#39;foo&#39;: &#39;error&#39;}' in resp
+
+    billing_date1 = agenda_pricing.billingdates.create(
+        date_start=datetime.date(2021, 9, 1),
+        label='Foo 1',
+    )
+    billing_date2 = agenda_pricing.billingdates.create(
+        date_start=datetime.date(2021, 9, 15),
+        label='Foo 2',
+    )
+
+    # check with billing dates
+    mock_pricing_data.reset_mock()
+    resp = app.get('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
+    resp.form['agenda'] = agenda.pk
+    resp.form['billing_date'] = billing_date1.pk
+    resp.form['user_external_id'] = 'user:1'
+    resp.form['adult_external_id'] = 'adult:1'
+    resp = resp.form.submit().follow()
+    assert mock_pricing_data.call_args_list == [
+        mock.call(
+            request=mock.ANY,
+            subscription={'date_start': '2021-09-02', 'date_end': '2021-09-03'},
+            user_external_id='user:1',
+            adult_external_id='adult:1',
+        )
+    ]
+
+    mock_pricing_data.reset_mock()
+    resp.form['billing_date'] = billing_date2.pk
+    resp = resp.form.submit().follow()
+    assert mock_pricing_data.call_args_list == []
+
+    mock_subscriptions.return_value = [
+        {
+            'date_start': '2021-09-01',
+            'date_end': '2021-09-15',
+        },
+    ]
+    mock_pricing_data.reset_mock()
+    resp = resp.form.submit().follow()
+    assert mock_pricing_data.call_args_list == []
+
+    mock_subscriptions.return_value = [
+        {
+            'date_start': '2021-09-15',
+            'date_end': '2021-09-16',
+        },
+    ]
+    mock_pricing_data.reset_mock()
+    resp = resp.form.submit().follow()
+    assert mock_pricing_data.call_args_list == [
+        mock.call(
+            request=mock.ANY,
+            subscription={'date_start': '2021-09-15', 'date_end': '2021-09-16'},
+            user_external_id='user:1',
+            adult_external_id='adult:1',
+        )
+    ]
+
+    mock_subscriptions.return_value = [
+        {
+            'date_start': '2021-09-30',
+            'date_end': '2021-10-01',
+        },
+    ]
+    mock_pricing_data.reset_mock()
+    resp = resp.form.submit().follow()
+    assert mock_pricing_data.call_args_list == [
+        mock.call(
+            request=mock.ANY,
+            subscription={'date_start': '2021-09-30', 'date_end': '2021-10-01'},
+            user_external_id='user:1',
+            adult_external_id='adult:1',
+        )
+    ]
+
+    # check with subscription_required False
+    agenda_pricing.subscription_required = False
+    agenda_pricing.save()
+    mock_pricing_data.reset_mock()
+    mock_subscriptions.reset_mock()
+    resp = app.get('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
+    assert 'agenda' not in resp.context['test_tool_form'].fields
+    resp.form['billing_date'] = billing_date1.pk
+    resp.form['user_external_id'] = 'user:1'
+    resp.form['adult_external_id'] = 'adult:1'
+    resp = resp.form.submit().follow()
+    assert mock_subscriptions.call_args_list == []
+    assert mock_pricing_data.call_args_list == [
+        mock.call(
+            request=mock.ANY,
+            subscription=None,
+            user_external_id='user:1',
+            adult_external_id='adult:1',
+        )
+    ]
+
+
+@mock.patch('lingo.pricing.forms.get_subscriptions')
+def test_detail_agenda_pricing_test_tool_for_flat_fee_schedule_subscription_error(
+    mock_subscriptions, app, admin_user
+):
+    agenda = Agenda.objects.create(label='Foo bar')
+    pricing = Pricing.objects.create(label='Foo bar')
+    agenda_pricing = AgendaPricing.objects.create(
+        pricing=pricing,
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=10, day=1),
+        flat_fee_schedule=True,
+    )
+    agenda_pricing.agendas.add(agenda)
+
+    mock_subscriptions.side_effect = ChronoError('foo bar')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/agenda-pricing/%s/' % agenda_pricing.pk)
+    resp.form['agenda'] = agenda.pk
+    resp.form['user_external_id'] = 'user:1'
+    resp.form['adult_external_id'] = 'adult:1'
+    resp = resp.form.submit().follow()
+    assert resp.context['test_tool_form'].errors['user_external_id'] == ['foo bar']
+
+
 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

