From 30caf652caff52cd6d44b737ff740953c60ad88f 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 10:36:42 +0200
Subject: [PATCH 08/14] pricing: rename & rewrite get_pricing_data method
 (#67675)

renamed into get_pricing_data_for_event, to prepare flat_fee_schedule
mode
---
 lingo/api/serializers.py                     | 13 +++--
 lingo/pricing/forms.py                       |  3 +-
 lingo/pricing/models.py                      | 31 ++++--------
 tests/api/test_pricing.py                    | 51 ++++++++++++++++----
 tests/pricing/manager/test_agenda_pricing.py | 38 ++++++++-------
 tests/pricing/test_models.py                 | 30 +++++-------
 6 files changed, 97 insertions(+), 69 deletions(-)

diff --git a/lingo/api/serializers.py b/lingo/api/serializers.py
index 8f09fcc..1a7e177 100644
--- a/lingo/api/serializers.py
+++ b/lingo/api/serializers.py
@@ -22,7 +22,7 @@ from rest_framework.exceptions import ValidationError
 
 from lingo.agendas.chrono import ChronoError, get_events, get_subscriptions
 from lingo.agendas.models import Agenda
-from lingo.pricing.models import AgendaPricing, PricingError
+from lingo.pricing.models import AgendaPricing, AgendaPricingNotFound, PricingError
 
 
 class CommaSeparatedStringField(serializers.ListField):
@@ -119,10 +119,13 @@ class PricingComputeSerializer(serializers.Serializer):
         event_slugs = sorted(self.serialized_events.keys())
         for event_slug in event_slugs:
             serialized_event = self.serialized_events[event_slug]
+            start_date = datetime.datetime.fromisoformat(serialized_event['start_datetime']).date()
+            agenda = self.agendas[serialized_event['agenda']]
             try:
-                pricing_data = AgendaPricing.get_pricing_data(
+                agenda_pricing = AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date)
+                pricing_data = agenda_pricing.get_pricing_data_for_event(
                     request=request,
-                    agenda=self.agendas[serialized_event['agenda']],
+                    agenda=agenda,
                     event=serialized_event,
                     subscription=self.event_subscriptions[event_slug],
                     check_status={
@@ -139,6 +142,10 @@ class PricingComputeSerializer(serializers.Serializer):
                         'pricing_data': pricing_data,
                     }
                 )
+            except AgendaPricingNotFound:
+                result.append(
+                    {'event': event_slug, 'error': _('No agenda pricing found for event %s') % event_slug}
+                )
             except PricingError as e:
                 result.append({'event': event_slug, 'error': e.details})
 
diff --git a/lingo/pricing/forms.py b/lingo/pricing/forms.py
index c3379e4..786f333 100644
--- a/lingo/pricing/forms.py
+++ b/lingo/pricing/forms.py
@@ -386,10 +386,9 @@ class PricingTestToolForm(forms.Form):
         if not self.serialized_event or not self.serialized_subscription:
             return
         try:
-            return AgendaPricing.get_pricing_data(
+            return self.agenda_pricing.get_pricing_data_for_event(
                 request=self.request,
                 agenda=self.agenda,
-                agenda_pricing=self.agenda_pricing,
                 event=self.serialized_event,
                 subscription=self.serialized_subscription,
                 check_status={
diff --git a/lingo/pricing/models.py b/lingo/pricing/models.py
index 72a7b59..fcbd75f 100644
--- a/lingo/pricing/models.py
+++ b/lingo/pricing/models.py
@@ -16,7 +16,6 @@
 
 import copy
 import dataclasses
-import datetime
 import decimal
 from typing import List
 
@@ -400,33 +399,24 @@ class AgendaPricing(models.Model):
 
         return created, agenda_pricing
 
-    @staticmethod
-    def get_pricing_data(
-        request,
-        agenda,
-        event,
-        subscription,
-        check_status,
-        booking,
-        user_external_id,
-        adult_external_id,
-        agenda_pricing=None,
+    def get_pricing_data_for_event(
+        self, request, agenda, event, subscription, check_status, booking, user_external_id, adult_external_id
     ):
-        agenda_pricing = agenda_pricing or AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
+        # compute pricing for an event
         data = {
             'event': event,
             'subscription': subscription,
             'booking': booking,
         }
-        context = agenda_pricing.get_pricing_context(
+        context = self.get_pricing_context(
             request=request,
             data=data,
             user_external_id=user_external_id,
             adult_external_id=adult_external_id,
         )
-        pricing, criterias = agenda_pricing.compute_pricing(context=context)
-        modifier = agenda_pricing.get_booking_modifier(agenda=agenda, check_status=check_status)
-        return agenda_pricing.aggregate_pricing_data(
+        pricing, criterias = self.compute_pricing(context=context)
+        modifier = self.get_booking_modifier(agenda=agenda, check_status=check_status)
+        return self.aggregate_pricing_data(
             pricing=pricing, criterias=criterias, context=context, modifier=modifier
         )
 
@@ -446,12 +436,11 @@ class AgendaPricing(models.Model):
         }
 
     @staticmethod
-    def get_agenda_pricing(agenda, event):
-        start_datetime = datetime.datetime.fromisoformat(event['start_datetime'])
+    def get_agenda_pricing(agenda, start_date):
         try:
             return agenda.agendapricings.get(
-                date_start__lte=start_datetime,
-                date_end__gt=start_datetime,
+                date_start__lte=start_date,
+                date_end__gt=start_date,
             )
         except (AgendaPricing.DoesNotExist, AgendaPricing.MultipleObjectsReturned):
             raise AgendaPricingNotFound
diff --git a/tests/api/test_pricing.py b/tests/api/test_pricing.py
index e965469..3b3508a 100644
--- a/tests/api/test_pricing.py
+++ b/tests/api/test_pricing.py
@@ -1,10 +1,11 @@
+import datetime
 from unittest import mock
 
 import pytest
 
 from lingo.agendas.chrono import ChronoError
 from lingo.agendas.models import Agenda
-from lingo.pricing.models import PricingError
+from lingo.pricing.models import AgendaPricing, Pricing, PricingError
 
 pytestmark = pytest.mark.django_db
 
@@ -145,12 +146,13 @@ def test_pricing_compute_subscriptions_error(mock_subscriptions, mock_events, ap
 
 @mock.patch('lingo.api.serializers.get_events')
 @mock.patch('lingo.api.serializers.get_subscriptions')
-@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data')
-def test_pricing_compute(mock_pricing_data, mock_subscriptions, mock_events, app, user):
+@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
+def test_pricing_compute(mock_pricing_data_event, mock_subscriptions, mock_events, app, user):
     agenda = Agenda.objects.create(label='Agenda')
     agenda2 = Agenda.objects.create(label='Agenda2')
     app.authorization = ('Basic', ('john.doe', 'password'))
 
+    # no subscription
     mock_events.return_value = [
         {'start_datetime': '2021-09-01T12:00:00+02:00', 'agenda': 'agenda', 'slug': 'event-bar-slug'}
     ]
@@ -169,9 +171,10 @@ def test_pricing_compute(mock_pricing_data, mock_subscriptions, mock_events, app
     assert resp.json['errors']['user_external_id'] == [
         'No subscription found for event agenda@event-bar-slug'
     ]
-    assert mock_pricing_data.call_args_list == []
+    assert mock_pricing_data_event.call_args_list == []
     assert mock_subscriptions.call_args_list == [mock.call('agenda', 'user:1')]
 
+    # no matching subscription
     mock_subscriptions.reset_mock()
     mock_subscriptions.return_value = [
         {
@@ -197,9 +200,10 @@ def test_pricing_compute(mock_pricing_data, mock_subscriptions, mock_events, app
     assert resp.json['errors']['user_external_id'] == [
         'No subscription found for event agenda@event-bar-slug'
     ]
-    assert mock_pricing_data.call_args_list == []
+    assert mock_pricing_data_event.call_args_list == []
     assert mock_subscriptions.call_args_list == [mock.call('agenda', 'user:1')]
 
+    # no matching subscription
     mock_events.return_value = [
         {'start_datetime': '2021-09-02T12:00:00+02:00', 'agenda': 'agenda', 'slug': 'event-bar-slug'},
         {'start_datetime': '2021-09-01T12:00:00+02:00', 'agenda': 'agenda2', 'slug': 'event-baz-slug'},
@@ -229,12 +233,13 @@ def test_pricing_compute(mock_pricing_data, mock_subscriptions, mock_events, app
     assert resp.json['errors']['user_external_id'] == [
         'No subscription found for event agenda2@event-baz-slug'
     ]
-    assert mock_pricing_data.call_args_list == []
+    assert mock_pricing_data_event.call_args_list == []
     assert mock_subscriptions.call_args_list == [
         mock.call('agenda', 'user:1'),
         mock.call('agenda2', 'user:1'),
     ]
 
+    # no agenda_pricing found
     mock_subscriptions.return_value = [
         {
             'date_start': '2021-08-01',
@@ -249,7 +254,7 @@ def test_pricing_compute(mock_pricing_data, mock_subscriptions, mock_events, app
             'date_end': '2021-09-03',
         },
     ]
-    mock_pricing_data.side_effect = [
+    mock_pricing_data_event.side_effect = [
         {'foo': 'baz'},
         {'foo': 'bar'},
     ]
@@ -261,11 +266,38 @@ def test_pricing_compute(mock_pricing_data, mock_subscriptions, mock_events, app
             'adult_external_id': 'adult:1',
         },
     )
+    assert resp.json['data'] == [
+        {
+            'event': 'agenda2@event-baz-slug',
+            'error': 'No agenda pricing found for event agenda2@event-baz-slug',
+        },
+        {
+            'event': 'agenda@event-bar-slug',
+            'error': 'No agenda pricing found for event agenda@event-bar-slug',
+        },
+    ]
+
+    # ok
+    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),
+    )
+    agenda_pricing.agendas.add(agenda, agenda2)
+    resp = app.get(
+        '/api/pricing/compute/',
+        params={
+            'slots': 'agenda@event-bar-slug, agenda2@event-baz-slug',
+            'user_external_id': 'user:1',
+            'adult_external_id': 'adult:1',
+        },
+    )
     assert resp.json['data'] == [
         {'event': 'agenda2@event-baz-slug', 'pricing_data': {'foo': 'baz'}},
         {'event': 'agenda@event-bar-slug', 'pricing_data': {'foo': 'bar'}},
     ]
-    assert mock_pricing_data.call_args_list == [
+    assert mock_pricing_data_event.call_args_list == [
         mock.call(
             request=mock.ANY,
             agenda=agenda2,
@@ -296,7 +328,8 @@ def test_pricing_compute(mock_pricing_data, mock_subscriptions, mock_events, app
         ),
     ]
 
-    mock_pricing_data.side_effect = [
+    # get_pricing_data_for_event with error
+    mock_pricing_data_event.side_effect = [
         PricingError(details={'foo': 'error'}),
         {'foo': 'bar'},
     ]
diff --git a/tests/pricing/manager/test_agenda_pricing.py b/tests/pricing/manager/test_agenda_pricing.py
index 02c9c1c..8c2865a 100644
--- a/tests/pricing/manager/test_agenda_pricing.py
+++ b/tests/pricing/manager/test_agenda_pricing.py
@@ -821,8 +821,10 @@ 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')
-def test_detail_agenda_pricing_test_tool(mock_pricing_data, mock_subscriptions, mock_event, app, admin_user):
+@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
+def test_detail_agenda_pricing_test_tool(
+    mock_pricing_data_event, 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(
@@ -847,7 +849,7 @@ def test_detail_agenda_pricing_test_tool(mock_pricing_data, mock_subscriptions,
     assert resp.context['test_tool_form'].errors['event_slug'] == [
         'This event takes place outside the period covered by this pricing'
     ]
-    assert mock_pricing_data.call_args_list == []
+    assert mock_pricing_data_event.call_args_list == []
     mock_event.return_value = {'start_datetime': '2021-10-01T12:00:00+02:00'}
     resp = resp.form.submit().follow()
     assert 'Computed pricing data' not in resp
@@ -870,7 +872,7 @@ def test_detail_agenda_pricing_test_tool(mock_pricing_data, mock_subscriptions,
     mock_event.reset_mock()
     resp.form['event_slug'] = 'foo-bar@foo'
     resp = resp.form.submit().follow()
-    assert mock_pricing_data.call_args_list == []
+    assert mock_pricing_data_event.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'
@@ -906,14 +908,13 @@ def test_detail_agenda_pricing_test_tool(mock_pricing_data, mock_subscriptions,
             'date_end': '2021-09-03',
         },
     ]
-    mock_pricing_data.return_value = {'foo': 'bar', 'pricing': Decimal('42')}
+    mock_pricing_data_event.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 == [
+    assert mock_pricing_data_event.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},
@@ -925,7 +926,7 @@ def test_detail_agenda_pricing_test_tool(mock_pricing_data, mock_subscriptions,
     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'})
+    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
@@ -982,9 +983,9 @@ 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')
+@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
 def test_detail_agenda_pricing_test_tool_booking_status(
-    mock_pricing_data, mock_subscriptions, mock_event, app, admin_user
+    mock_pricing_data_event, mock_subscriptions, mock_event, app, admin_user
 ):
     agenda = Agenda.objects.create(label='Foo bar')
     pricing = Pricing.objects.create(label='Foo bar')
@@ -1032,28 +1033,31 @@ def test_detail_agenda_pricing_test_tool_booking_status(
         ('absence', False, 'Absence'),
         ('absence::foo-absence-reason', False, 'Absence (Foo absence reason)'),
     ]
-    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
+    assert mock_pricing_data_event.call_args_list[0][1]['check_status'] == {
         'check_type': None,
         'status': 'presence',
     }
 
-    mock_pricing_data.reset_mock()
+    mock_pricing_data_event.reset_mock()
     resp.form['booking_status'] = 'presence::foo-presence-reason'
     resp = resp.form.submit().follow()
-    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
+    assert mock_pricing_data_event.call_args_list[0][1]['check_status'] == {
         'check_type': 'foo-presence-reason',
         'status': 'presence',
     }
 
-    mock_pricing_data.reset_mock()
+    mock_pricing_data_event.reset_mock()
     resp.form['booking_status'] = 'absence'
     resp = resp.form.submit().follow()
-    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {'check_type': None, 'status': 'absence'}
+    assert mock_pricing_data_event.call_args_list[0][1]['check_status'] == {
+        'check_type': None,
+        'status': 'absence',
+    }
 
-    mock_pricing_data.reset_mock()
+    mock_pricing_data_event.reset_mock()
     resp.form['booking_status'] = 'absence::foo-absence-reason'
     resp = resp.form.submit().follow()
-    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
+    assert mock_pricing_data_event.call_args_list[0][1]['check_status'] == {
         'check_type': 'foo-absence-reason',
         'status': 'absence',
     }
diff --git a/tests/pricing/test_models.py b/tests/pricing/test_models.py
index 69eaa5b..8fd60a2 100644
--- a/tests/pricing/test_models.py
+++ b/tests/pricing/test_models.py
@@ -283,13 +283,11 @@ def test_agenda_pricing_duplicate_slugs():
 def test_get_agenda_pricing():
     agenda = Agenda.objects.create(label='Foo bar')
     pricing = Pricing.objects.create(label='Foo bar')
-    event = {
-        'start_datetime': make_aware(datetime.datetime(2021, 9, 15, 12, 00)).isoformat(),
-    }
+    start_date = datetime.datetime(2021, 9, 15)
 
     # not found
     with pytest.raises(AgendaPricingNotFound):
-        AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
+        AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date)
 
     # ok
     agenda_pricing = AgendaPricing.objects.create(
@@ -298,7 +296,7 @@ def test_get_agenda_pricing():
         date_end=datetime.date(year=2021, month=10, day=1),
     )
     agenda_pricing.agendas.add(agenda)
-    assert AgendaPricing.get_agenda_pricing(agenda=agenda, event=event) == agenda_pricing
+    assert AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date) == agenda_pricing
 
     # more than one matching
     agenda_pricing = AgendaPricing.objects.create(
@@ -308,39 +306,37 @@ def test_get_agenda_pricing():
     )
     agenda_pricing.agendas.add(agenda)
     with pytest.raises(AgendaPricingNotFound):
-        AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
+        AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date)
 
 
 @pytest.mark.parametrize(
     'event_date, found',
     [
         # just before first day
-        ((2021, 8, 31, 12, 00), False),
+        ((2021, 8, 31), False),
         # first day
-        ((2021, 9, 1, 12, 00), True),
+        ((2021, 9, 1), True),
         # last day
-        ((2021, 9, 30, 12, 00), True),
+        ((2021, 9, 30), True),
         # just after last day
-        ((2021, 10, 1, 12, 00), False),
+        ((2021, 10, 1), False),
     ],
 )
 def test_get_agenda_pricing_event_date(event_date, found):
     agenda = Agenda.objects.create(label='Foo bar')
     pricing = Pricing.objects.create(label='Foo bar')
-    event = {
-        'start_datetime': make_aware(datetime.datetime(*event_date)).isoformat(),
-    }
     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),
     )
     agenda_pricing.agendas.add(agenda)
+    start_date = datetime.date(*event_date)
     if found:
-        assert AgendaPricing.get_agenda_pricing(agenda=agenda, event=event) == agenda_pricing
+        assert AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date) == agenda_pricing
     else:
         with pytest.raises(AgendaPricingNotFound):
-            AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
+            AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date)
 
 
 @mock.patch('requests.Session.send', side_effect=mocked_requests_send)
@@ -931,7 +927,7 @@ def test_get_booking_modifier_booking_presence():
     }
 
 
-def test_get_pricing_data(context):
+def test_get_pricing_data_for_event(context):
     agenda = Agenda.objects.create(label='Foo bar')
     category = CriteriaCategory.objects.create(label='Foo', slug='foo')
     criteria = Criteria.objects.create(label='Bar', slug='bar', condition='True', category=category)
@@ -953,7 +949,7 @@ def test_get_pricing_data(context):
         },
     )
     agenda_pricing.agendas.add(agenda)
-    assert AgendaPricing.get_pricing_data(
+    assert agenda_pricing.get_pricing_data_for_event(
         request=context['request'],
         agenda=agenda,
         event={'start_datetime': make_aware(datetime.datetime(2021, 9, 15, 12, 00)).isoformat()},
-- 
2.35.1

