pricing: apply modifier according to booking status (#64565)

This commit is contained in:
Lauréline Guérin 2022-04-29 15:22:04 +02:00
parent 301d280c9e
commit e504199eab
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
2 changed files with 711 additions and 32 deletions

View File

@ -22,6 +22,7 @@ from django.template import Context, Template, TemplateSyntaxError
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from chrono.agendas.models import Agenda, Booking, Subscription
from chrono.utils.misc import generate_slug
@ -47,6 +48,26 @@ class PricingDataFormatError(PricingError):
pass
class PricingEventNotCheckedError(PricingError):
pass
class PricingBookingNotCheckedError(PricingError):
pass
class PricingSubscriptionError(PricingError):
pass
class PricingMultipleBookingError(PricingError):
pass
class PricingBookingCheckTypeError(PricingError):
pass
class CriteriaCategory(models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
@ -156,7 +177,7 @@ class PricingCriteriaCategory(models.Model):
class AgendaPricing(models.Model):
agenda = models.ForeignKey('agendas.Agenda', on_delete=models.CASCADE)
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
pricing = models.ForeignKey(Pricing, on_delete=models.CASCADE)
date_start = models.DateField()
date_end = models.DateField()
@ -167,10 +188,22 @@ class AgendaPricing(models.Model):
agenda_pricing = AgendaPricing.get_agenda_pricing(event)
context = agenda_pricing.get_pricing_context(event, user_external_id, adult_external_id)
pricing, criterias = agenda_pricing.compute_pricing(context)
modifier = agenda_pricing.get_booking_modifier(event, user_external_id)
return agenda_pricing.aggregate_pricing_data(pricing, criterias, context, modifier)
def aggregate_pricing_data(self, pricing, criterias, context, modifier):
if modifier['modifier_type'] == 'fixed':
pricing_amount = modifier['modifier_fixed']
else:
pricing_amount = pricing * modifier['modifier_rate'] / 100
return {
'pricing': pricing,
'criterias': criterias,
'context': context,
'pricing': pricing_amount,
'calculation_details': {
'pricing': pricing,
'criterias': criterias,
'context': context,
},
'booking_details': modifier,
}
@staticmethod
@ -184,6 +217,16 @@ class AgendaPricing(models.Model):
except (AgendaPricing.DoesNotExist, AgendaPricing.MultipleObjectsReturned):
raise AgendaPricingNotFound
def get_subscription(self, event, user_external_id):
try:
return self.agenda.subscriptions.get(
user_external_id=user_external_id,
date_start__lte=event.start_datetime,
date_end__gt=event.start_datetime,
)
except (Subscription.DoesNotExist, Subscription.MultipleObjectsReturned):
raise PricingSubscriptionError
def get_pricing_context(self, event, user_external_id, adult_external_id):
# FIXME: compute agenda pricing variables, add event data
return {
@ -227,3 +270,85 @@ class AgendaPricing(models.Model):
raise PricingDataFormatError(details={'pricing': pricing_data, 'wanted': 'decimal'})
return pricing, criterias
def get_booking_modifier(self, event, user_external_id):
# event must be checked
if event.checked is False:
raise PricingEventNotCheckedError
# search for an available subscription
self.get_subscription(event, user_external_id)
# search for a booking
try:
booking = event.booking_set.get(user_external_id=user_external_id)
except Booking.DoesNotExist:
# no booking
return {
'status': 'not-booked',
'modifier_type': 'rate',
'modifier_rate': 0,
}
except Booking.MultipleObjectsReturned:
raise PricingMultipleBookingError
# booking cancelled
if booking.cancellation_datetime is not None:
return {
'status': 'cancelled',
'modifier_type': 'rate',
'modifier_rate': 0,
}
# booking not cancelled, is must be checked
if booking.user_was_present is None:
raise PricingBookingNotCheckedError
status = 'presence' if booking.user_was_present else 'absence'
# no check_type, default rates
if booking.user_check_type is None:
return {
'status': status,
'check_type_group': None,
'check_type': None,
'modifier_type': 'rate',
'modifier_rate': 100 if booking.user_was_present else 0,
}
check_type = booking.user_check_type
kind_mapping = {'presence': True, 'absence': False}
# check_type kind and user_was_present mismatch
if kind_mapping[check_type.kind] != booking.user_was_present:
raise PricingBookingCheckTypeError(
details={
'check_type_group': check_type.group.slug,
'check_type': check_type.slug,
'reason': 'wrong-kind',
}
)
# get pricing modifier
if check_type.pricing is not None:
return {
'status': status,
'check_type_group': check_type.group.slug,
'check_type': check_type.slug,
'modifier_type': 'fixed',
'modifier_fixed': check_type.pricing,
}
if check_type.pricing_rate is not None:
return {
'status': status,
'check_type_group': check_type.group.slug,
'check_type': check_type.slug,
'modifier_type': 'rate',
'modifier_rate': check_type.pricing_rate,
}
# pricing not found
raise PricingBookingCheckTypeError(
details={
'check_type_group': check_type.group.slug,
'check_type': check_type.slug,
'reason': 'not-configured',
}
)

View File

@ -1,9 +1,9 @@
import datetime
import pytest
from django.utils.timezone import make_aware
from django.utils.timezone import make_aware, now
from chrono.agendas.models import Agenda, Event
from chrono.agendas.models import Agenda, Booking, CheckType, CheckTypeGroup, Event, Subscription
from chrono.pricing.models import (
AgendaPricing,
AgendaPricingNotFound,
@ -11,9 +11,14 @@ from chrono.pricing.models import (
CriteriaCategory,
CriteriaConditionNotFound,
Pricing,
PricingBookingCheckTypeError,
PricingBookingNotCheckedError,
PricingCriteriaCategory,
PricingDataError,
PricingDataFormatError,
PricingEventNotCheckedError,
PricingMultipleBookingError,
PricingSubscriptionError,
)
pytestmark = pytest.mark.django_db
@ -199,32 +204,6 @@ def test_get_agenda_pricing_event_date(event_date, found):
AgendaPricing.get_agenda_pricing(event)
def test_get_pricing_data():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
agenda=agenda, start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)), places=10
)
category = CriteriaCategory.objects.create(label='Foo', slug='foo')
criteria = Criteria.objects.create(label='Bar', slug='bar', condition='True', category=category)
pricing = Pricing.objects.create(label='Foo bar')
pricing.criterias.add(criteria)
pricing.categories.add(category, through_defaults={'order': 1})
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),
pricing_data={
'foo:bar': 42,
},
)
assert AgendaPricing.get_pricing_data(event, 'child:42', 'parent:35') == {
'pricing': 42,
'criterias': {'foo': 'bar'},
'context': {'domicile': 'commune', 'qf': 2},
}
def test_get_pricing_context():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
@ -380,3 +359,578 @@ def test_compute_pricing():
7,
{'domicile': 'dom-1', 'qf': 'qf-0'},
)
def test_get_booking_modifier_event_not_checked():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
agenda=agenda, start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)), places=10
)
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),
)
with pytest.raises(PricingEventNotCheckedError):
agenda_pricing.get_booking_modifier(event, 'child:42')
def test_get_booking_modifier_no_subscription():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
places=10,
checked=True,
)
Subscription.objects.create(
agenda=agenda,
user_external_id='child:35', # another user
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
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),
)
with pytest.raises(PricingSubscriptionError):
agenda_pricing.get_booking_modifier(event, 'child:42')
# more than one subscription found !
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
with pytest.raises(PricingSubscriptionError):
agenda_pricing.get_booking_modifier(event, 'child:42')
@pytest.mark.parametrize(
'event_date, success',
[
# just before first day
((2021, 8, 31, 12, 00), False),
# first day
((2021, 9, 1, 12, 00), True),
# last day
((2021, 9, 30, 12, 00), True),
# just after last day
((2021, 10, 1, 12, 00), False),
],
)
def test_get_booking_modifier_subscription_date(event_date, success):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
event = Event.objects.create(
agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10, checked=True
)
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),
)
if success:
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'not-booked',
'modifier_type': 'rate',
'modifier_rate': 0,
}
else:
with pytest.raises(PricingSubscriptionError):
agenda_pricing.get_booking_modifier(event, 'child:42')
def test_get_booking_modifier_no_booking():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
places=10,
checked=True,
)
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),
)
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'not-booked',
'modifier_type': 'rate',
'modifier_rate': 0,
}
# more than one booking found !
Booking.objects.create(event=event, user_external_id='child:42')
Booking.objects.create(event=event, user_external_id='child:42')
with pytest.raises(PricingMultipleBookingError):
agenda_pricing.get_booking_modifier(event, 'child:42')
def test_get_booking_modifier_booking_cancelled():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
places=10,
checked=True,
)
Booking.objects.create(event=event, user_external_id='child:42', cancellation_datetime=now())
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),
)
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'cancelled',
'modifier_type': 'rate',
'modifier_rate': 0,
}
def test_get_booking_modifier_booking_not_checked():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
places=10,
checked=True,
)
Booking.objects.create(event=event, user_external_id='child:42')
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),
)
with pytest.raises(PricingBookingNotCheckedError):
agenda_pricing.get_booking_modifier(event, 'child:42')
def test_get_booking_modifier_booking_absence():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
places=10,
checked=True,
)
booking = Booking.objects.create(event=event, user_external_id='child:42', user_was_present=False)
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),
)
# no check type
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'absence',
'check_type_group': None,
'check_type': None,
'modifier_type': 'rate',
'modifier_rate': 0,
}
# check_type but incomplete configuration
group = CheckTypeGroup.objects.create(label='Foo bar')
check_type = CheckType.objects.create(label='Foo reason', group=group, kind='absence')
booking.user_check_type = check_type
booking.save()
with pytest.raises(PricingBookingCheckTypeError) as e:
agenda_pricing.get_booking_modifier(event, 'child:42')
assert e.value.details == {
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'reason': 'not-configured',
}
check_type.pricing = 42
check_type.save()
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'absence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'fixed',
'modifier_fixed': 42,
}
check_type.pricing = 0
check_type.save()
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'absence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'fixed',
'modifier_fixed': 0,
}
check_type.pricing = None
check_type.pricing_rate = 20
check_type.save()
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'absence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'rate',
'modifier_rate': 20,
}
check_type.pricing_rate = 0
check_type.save()
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'absence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'rate',
'modifier_rate': 0,
}
# bad check type kind
check_type.kind = 'presence'
check_type.save()
with pytest.raises(PricingBookingCheckTypeError) as e:
agenda_pricing.get_booking_modifier(event, 'child:42')
assert e.value.details == {
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'reason': 'wrong-kind',
}
def test_get_booking_modifier_booking_presence():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
places=10,
checked=True,
)
booking = Booking.objects.create(event=event, user_external_id='child:42', user_was_present=True)
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),
)
# no check type
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'presence',
'check_type_group': None,
'check_type': None,
'modifier_type': 'rate',
'modifier_rate': 100,
}
# check_type but incomplete configuration
group = CheckTypeGroup.objects.create(label='Foo bar')
check_type = CheckType.objects.create(label='Foo reason', group=group, kind='presence')
booking.user_check_type = check_type
booking.save()
with pytest.raises(PricingBookingCheckTypeError) as e:
agenda_pricing.get_booking_modifier(event, 'child:42')
assert e.value.details == {
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'reason': 'not-configured',
}
check_type.pricing = 42
check_type.save()
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'presence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'fixed',
'modifier_fixed': 42,
}
check_type.pricing = 0
check_type.save()
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'presence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'fixed',
'modifier_fixed': 0,
}
check_type.pricing = None
check_type.pricing_rate = 150
check_type.save()
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'presence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'rate',
'modifier_rate': 150,
}
check_type.pricing_rate = 0
check_type.save()
assert agenda_pricing.get_booking_modifier(event, 'child:42') == {
'status': 'presence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'rate',
'modifier_rate': 0,
}
# bad check type kind
check_type.kind = 'absence'
check_type.save()
with pytest.raises(PricingBookingCheckTypeError) as e:
agenda_pricing.get_booking_modifier(event, 'child:42')
assert e.value.details == {
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'reason': 'wrong-kind',
}
def test_get_pricing_data():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2021, 9, 15, 12, 00)),
places=10,
checked=True,
)
Subscription.objects.create(
agenda=agenda,
user_external_id='child:42',
date_start=datetime.date(year=2021, month=9, day=1),
date_end=datetime.date(year=2021, month=10, day=1),
)
category = CriteriaCategory.objects.create(label='Foo', slug='foo')
criteria = Criteria.objects.create(label='Bar', slug='bar', condition='True', category=category)
pricing = Pricing.objects.create(label='Foo bar')
pricing.criterias.add(criteria)
pricing.categories.add(category, through_defaults={'order': 1})
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),
pricing_data={
'foo:bar': 42,
},
)
assert AgendaPricing.get_pricing_data(event, 'child:42', 'parent:35') == {
'pricing': 0,
'calculation_details': {
'pricing': 42,
'criterias': {'foo': 'bar'},
'context': {'domicile': 'commune', 'qf': 2},
},
'booking_details': {
'status': 'not-booked',
'modifier_type': 'rate',
'modifier_rate': 0,
},
}
@pytest.mark.parametrize(
'modifier, pricing_amount',
[
# not booked
(
{
'status': 'not-booked',
'modifier_type': 'rate',
'modifier_rate': 0,
},
0,
),
# cancelled
(
{
'status': 'cancelled',
'modifier_type': 'rate',
'modifier_rate': 0,
},
0,
),
# absence
(
{
'status': 'absence',
'check_type_group': None,
'check_type': None,
'modifier_type': 'rate',
'modifier_rate': 0,
},
0,
),
(
{
'status': 'absence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'fixed',
'modifier_fixed': 35,
},
35,
),
(
{
'status': 'absence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'fixed',
'modifier_fixed': 0,
},
0,
),
(
{
'status': 'absence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'rate',
'modifier_rate': 20,
},
8.4,
),
(
{
'status': 'absence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'rate',
'modifier_rate': 0,
},
0,
),
# presence
(
{
'status': 'presence',
'check_type_group': None,
'check_type': None,
'modifier_type': 'rate',
'modifier_rate': 100,
},
42,
),
(
{
'status': 'presence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'fixed',
'modifier_fixed': 35,
},
35,
),
(
{
'status': 'presence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'fixed',
'modifier_fixed': 0,
},
0,
),
(
{
'status': 'presence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'rate',
'modifier_rate': 150,
},
63,
),
(
{
'status': 'presence',
'check_type_group': 'foo-bar',
'check_type': 'foo-reason',
'modifier_type': 'rate',
'modifier_rate': 0,
},
0,
),
],
)
def test_aggregate_pricing_data(modifier, pricing_amount):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
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),
)
assert agenda_pricing.aggregate_pricing_data(
pricing=42, criterias={'foo': 'bar'}, context={'domicile': 'commune', 'qf': 2}, modifier=modifier
) == {
'pricing': pricing_amount,
'calculation_details': {
'pricing': 42,
'criterias': {'foo': 'bar'},
'context': {'domicile': 'commune', 'qf': 2},
},
'booking_details': modifier,
}