From 5d43627364d0c1d0201d2d396b2ed35024256ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= Date: Thu, 28 Jul 2022 10:36:42 +0200 Subject: [PATCH] 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 881c24f..f10837f 100644 --- a/lingo/pricing/forms.py +++ b/lingo/pricing/forms.py @@ -389,10 +389,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 '

Pricing: 42.00

' in resp assert '
{'foo': 'bar', 'pricing': Decimal('42')}
' 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 '
{'error': {'foo': 'error'}}
' 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()},