From 2389a3f70653580b1d4e93b5cb0b9fae3608dc9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= Date: Wed, 6 Jul 2022 15:12:34 +0200 Subject: [PATCH] pricing: format pricing_data to ignore category ordering (#66899) --- lingo/pricing/models.py | 96 +++++++------- tests/pricing/manager/test_agenda.py | 8 +- tests/pricing/test_models.py | 179 +++++++++++++++++++++------ 3 files changed, 201 insertions(+), 82 deletions(-) diff --git a/lingo/pricing/models.py b/lingo/pricing/models.py index 8e42436..fb7ca70 100644 --- a/lingo/pricing/models.py +++ b/lingo/pricing/models.py @@ -425,6 +425,30 @@ class AgendaPricing(models.Model): context['adult_external_raw_id'] = adult_external_id.split(':')[1] return self.pricing.get_extra_variables(request, context) + def format_pricing_data(self): + # format data to ignore category ordering + + def _format(data): + if not data: + return + + if not isinstance(data, dict): + yield [], data + return + + for criteria, val in data.items(): + result = list(_format(val)) + for criterias, pricing in result: + yield [criteria] + criterias, pricing + + return { + self.format_pricing_data_key(criterias): pricing + for criterias, pricing in _format(self.pricing_data) + } + + def format_pricing_data_key(self, values): + return '||'.join(sorted(values)) + def compute_pricing(self, context): criterias = {} categories = [] @@ -449,23 +473,17 @@ class AgendaPricing(models.Model): criterias[category.slug] = default_criterias[0].slug # now search for pricing values matching found criterias - pricing_data = self.pricing_data - # for each category (ordered) - for category in categories: - criteria = criterias[category] - if not isinstance(pricing_data, dict): - raise PricingDataFormatError( - details={'category': category, 'pricing': pricing_data, 'wanted': 'dict'} - ) - key = '%s:%s' % (category, criteria) - if key not in pricing_data: - raise PricingDataError(details={'category': category, 'criteria': criteria}) - pricing_data = pricing_data[key] + pricing_data = self.format_pricing_data() + pricing = pricing_data.get( + self.format_pricing_data_key(['%s:%s' % (k, v) for k, v in criterias.items()]) + ) + if pricing is None: + raise PricingDataError(details={'criterias': criterias}) try: - pricing = decimal.Decimal(pricing_data) + pricing = decimal.Decimal(pricing) except (decimal.InvalidOperation, ValueError, TypeError): - raise PricingDataFormatError(details={'pricing': pricing_data, 'wanted': 'decimal'}) + raise PricingDataFormatError(details={'pricing': pricing, 'wanted': 'decimal'}) return pricing, criterias @@ -560,7 +578,7 @@ class AgendaPricing(models.Model): def iter_pricing_matrix(self): categories = self.pricing.categories.all().order_by('pricingcriteriacategory__order')[:3] - pricing_data = self.pricing_data or {} + pricing_data = self.format_pricing_data() if not categories: return @@ -578,7 +596,7 @@ class AgendaPricing(models.Model): yield self.get_pricing_matrix( main_criteria=criteria, categories=categories[1:], - pricing_data=pricing_data.get(criteria.identifier) or {}, + pricing_data=pricing_data, ) def get_pricing_matrix(self, main_criteria, categories, pricing_data): @@ -587,41 +605,35 @@ class AgendaPricing(models.Model): rows=[], ) - def get_pricing_matrix_cell(criteria_2, criteria_3, _pricing_data): + def get_pricing_matrix_cell(criteria_2, criteria_3): + criterias = [main_criteria, criteria_2, criteria_3] + key = self.format_pricing_data_key([c.identifier for c in criterias if c]) try: - value = decimal.Decimal(str(_pricing_data.get(criteria_3.identifier))) + value = decimal.Decimal(str(pricing_data.get(key))) except (decimal.InvalidOperation, ValueError, TypeError): value = None return PricingMatrixCell(criteria=criteria_2, value=value) if len(categories) < 2: - rows = [ - PricingMatrixRow( - criteria=criteria, - cells=[get_pricing_matrix_cell(None, criteria, pricing_data)], - ) - for criteria in self.pricing.criterias.all() - if criteria.category == categories[0] - ] - matrix.rows = rows + criterias_2 = [None] + criterias_3 = [c for c in self.pricing.criterias.all() if c.category == categories[0]] else: criterias_2 = [c for c in self.pricing.criterias.all() if c.category == categories[0]] criterias_3 = [c for c in self.pricing.criterias.all() if c.category == categories[1]] - rows = [ - PricingMatrixRow( - criteria=criteria_3, - cells=[ - get_pricing_matrix_cell( - criteria_2, - criteria_3, - pricing_data.get(criteria_2.identifier) or {}, - ) - for criteria_2 in criterias_2 - ], - ) - for criteria_3 in criterias_3 - ] - matrix.rows = rows + rows = [ + PricingMatrixRow( + criteria=criteria_3, + cells=[ + get_pricing_matrix_cell( + criteria_2, + criteria_3, + ) + for criteria_2 in criterias_2 + ], + ) + for criteria_3 in criterias_3 + ] + matrix.rows = rows return matrix diff --git a/tests/pricing/manager/test_agenda.py b/tests/pricing/manager/test_agenda.py index 106fe1d..0c7e2c3 100644 --- a/tests/pricing/manager/test_agenda.py +++ b/tests/pricing/manager/test_agenda.py @@ -333,8 +333,8 @@ def test_detail_agenda_pricing_2_categories(app, admin_user): agenda = Agenda.objects.create(label='Foo bar') pricing = Pricing.objects.create(label='Foo bar') - pricing.categories.add(category2, through_defaults={'order': 2}) - pricing.categories.add(category3, through_defaults={'order': 3}) + pricing.categories.add(category2, through_defaults={'order': 1}) + pricing.categories.add(category3, through_defaults={'order': 2}) pricing.criterias.set(Criteria.objects.all()) agenda_pricing = AgendaPricing.objects.create( agenda=agenda, @@ -815,8 +815,8 @@ def test_edit_agenda_pricing_matrix_2_categories(app, admin_user): agenda = Agenda.objects.create(label='Foo bar') pricing = Pricing.objects.create(label='Foo bar') - pricing.categories.add(category2, through_defaults={'order': 2}) - pricing.categories.add(category3, through_defaults={'order': 3}) + pricing.categories.add(category2, through_defaults={'order': 1}) + pricing.categories.add(category3, through_defaults={'order': 2}) pricing.criterias.set(Criteria.objects.all()) agenda_pricing = AgendaPricing.objects.create( agenda=agenda, diff --git a/tests/pricing/test_models.py b/tests/pricing/test_models.py index 9c8808b..5a77b75 100644 --- a/tests/pricing/test_models.py +++ b/tests/pricing/test_models.py @@ -424,9 +424,9 @@ def test_compute_pricing(): label='Else 1', slug='else-1', category=category, default=True ) pricing.criterias.add(default_criteria1) - with pytest.raises(PricingDataFormatError) as e: + with pytest.raises(PricingDataError) as e: agenda_pricing.compute_pricing(context={'qf': 2}) - assert e.value.details == {'category': 'qf', 'pricing': None, 'wanted': 'dict'} + assert e.value.details == {'criterias': {'qf': 'else-1'}} # with more than one default criteria, fail default_criteria2 = Criteria.objects.create( label='Else 2', slug='else-2', category=category, default=True @@ -442,9 +442,9 @@ def test_compute_pricing(): criteria1.save() criteria2.condition = 'qf >= 1' criteria2.save() - with pytest.raises(PricingDataFormatError) as e: + with pytest.raises(PricingDataError) as e: agenda_pricing.compute_pricing(context={'qf': 2}) - assert e.value.details == {'category': 'qf', 'pricing': None, 'wanted': 'dict'} + assert e.value.details == {'criterias': {'qf': 'qf-1'}} # criteria not found in pricing_data agenda_pricing.pricing_data = { @@ -453,10 +453,10 @@ def test_compute_pricing(): agenda_pricing.save() with pytest.raises(PricingDataError) as e: agenda_pricing.compute_pricing(context={'qf': 2}) - assert e.value.details == {'category': 'qf', 'criteria': 'qf-1'} + assert e.value.details == {'criterias': {'qf': 'qf-1'}} # criteria found, but value is wrong - for value in ['foo', [], {}]: + for value in ['foo', ['foo']]: agenda_pricing.pricing_data = { 'qf:qf-0': 42, 'qf:qf-1': value, @@ -465,6 +465,15 @@ def test_compute_pricing(): with pytest.raises(PricingDataFormatError) as e: agenda_pricing.compute_pricing(context={'qf': 2}) assert e.value.details == {'pricing': value, 'wanted': 'decimal'} + for value in [[], {}]: + agenda_pricing.pricing_data = { + 'qf:qf-0': 42, + 'qf:qf-1': value, + } + agenda_pricing.save() + with pytest.raises(PricingDataError) as e: + agenda_pricing.compute_pricing(context={'qf': 2}) + assert e.value.details == {'criterias': {'qf': 'qf-1'}} # correct value (decimal) agenda_pricing.pricing_data = { @@ -486,7 +495,7 @@ def test_compute_pricing(): pricing.criterias.add(criteria1) pricing.criterias.add(criteria2) - # wrong definition + # correct definition agenda_pricing.pricing_data = { 'domicile:dom-0': { 'qf:qf-0': 3, @@ -498,10 +507,24 @@ def test_compute_pricing(): }, } agenda_pricing.save() - with pytest.raises(PricingDataError) as e: - agenda_pricing.compute_pricing(context={'qf': 2, 'domicile': 'commune'}) + assert agenda_pricing.compute_pricing(context={'qf': 2, 'domicile': 'commune'}) == ( + 5, + {'domicile': 'dom-0', 'qf': 'qf-1'}, + ) + assert agenda_pricing.compute_pricing(context={'qf': 0, 'domicile': 'commune'}) == ( + 3, + {'domicile': 'dom-0', 'qf': 'qf-0'}, + ) + assert agenda_pricing.compute_pricing(context={'qf': 2, 'domicile': 'ext'}) == ( + 10, + {'domicile': 'else', 'qf': 'qf-1'}, + ) + assert agenda_pricing.compute_pricing(context={'qf': 0, 'domicile': 'ext'}) == ( + 7, + {'domicile': 'else', 'qf': 'qf-0'}, + ) - # reorder categories, so the definition is correct + # category ordering doesn't matter PricingCriteriaCategory.objects.filter(pricing=pricing, category=category).update(order=2) PricingCriteriaCategory.objects.filter(pricing=pricing, category=category2).update(order=1) assert agenda_pricing.compute_pricing(context={'qf': 2, 'domicile': 'commune'}) == ( @@ -522,6 +545,62 @@ def test_compute_pricing(): ) +def test_format_pricing_data(): + 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), + ) + assert agenda_pricing.format_pricing_data() == {} + + agenda_pricing.pricing_data = { + 'cat-1:crit-1-1': { + 'cat-2:crit-2-1': { + 'cat-3:crit-3-1': 111, + 'cat-3:crit-3-3': 'not-a-decimal', + 'cat-3:crit-3-4': 114, + }, + 'cat-2:crit-2-3': { + 'cat-3:crit-3-2': 132, + }, + }, + 'cat-1:crit-1-2': { + 'cat-2:crit-2-2': { + 'cat-3:crit-3-3': 223, + }, + }, + } + agenda_pricing.save() + assert agenda_pricing.format_pricing_data() == { + 'cat-1:crit-1-1||cat-2:crit-2-1||cat-3:crit-3-1': 111, + 'cat-1:crit-1-1||cat-2:crit-2-1||cat-3:crit-3-3': 'not-a-decimal', + 'cat-1:crit-1-1||cat-2:crit-2-1||cat-3:crit-3-4': 114, + 'cat-1:crit-1-1||cat-2:crit-2-3||cat-3:crit-3-2': 132, + 'cat-1:crit-1-2||cat-2:crit-2-2||cat-3:crit-3-3': 223, + } + + agenda_pricing.pricing_data = {'foo': 42} + agenda_pricing.save() + assert agenda_pricing.format_pricing_data() == {'foo': 42} + + # wrong data + agenda_pricing.pricing_data = 'foo' + agenda_pricing.save() + assert agenda_pricing.format_pricing_data() == {'': 'foo'} + agenda_pricing.pricing_data = [] + agenda_pricing.save() + assert agenda_pricing.format_pricing_data() == {} + agenda_pricing.pricing_data = ['foo'] + agenda_pricing.save() + assert agenda_pricing.format_pricing_data() == {'': ['foo']} + agenda_pricing.pricing_data = {'foo': []} + agenda_pricing.save() + assert agenda_pricing.format_pricing_data() == {} + + def test_get_booking_modifier_unknown_check_status(): agenda = Agenda.objects.create(label='Foo bar') pricing = Pricing.objects.create(label='Foo bar') @@ -1131,77 +1210,105 @@ def test_agenda_pricing_iter_pricing_matrix_3_categories(): }, } agenda_pricing.save() + # category ordering doesn't matter + PricingCriteriaCategory.objects.filter(pricing=pricing, category=category1).update(order=2) + PricingCriteriaCategory.objects.filter(pricing=pricing, category=category2).update(order=1) assert list(agenda_pricing.iter_pricing_matrix()) == [ PricingMatrix( - criteria=criteria11, + criteria=criteria21, rows=[ PricingMatrixRow( criteria=criteria31, cells=[ - PricingMatrixCell(criteria=criteria21, value=111), - PricingMatrixCell(criteria=criteria22, value=None), - PricingMatrixCell(criteria=criteria23, value=None), + PricingMatrixCell(criteria=criteria11, value=111), + PricingMatrixCell(criteria=criteria12, value=None), ], ), PricingMatrixRow( criteria=criteria32, cells=[ - PricingMatrixCell(criteria=criteria21, value=None), - PricingMatrixCell(criteria=criteria22, value=None), - PricingMatrixCell(criteria=criteria23, value=132), + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=None), ], ), PricingMatrixRow( criteria=criteria33, cells=[ - PricingMatrixCell(criteria=criteria21, value=None), - PricingMatrixCell(criteria=criteria22, value=None), - PricingMatrixCell(criteria=criteria23, value=None), + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=None), ], ), PricingMatrixRow( criteria=criteria34, cells=[ - PricingMatrixCell(criteria=criteria21, value=114), - PricingMatrixCell(criteria=criteria22, value=None), - PricingMatrixCell(criteria=criteria23, value=None), + PricingMatrixCell(criteria=criteria11, value=114), + PricingMatrixCell(criteria=criteria12, value=None), ], ), ], ), PricingMatrix( - criteria=criteria12, + criteria=criteria22, rows=[ PricingMatrixRow( criteria=criteria31, cells=[ - PricingMatrixCell(criteria=criteria21, value=None), - PricingMatrixCell(criteria=criteria22, value=None), - PricingMatrixCell(criteria=criteria23, value=None), + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=None), ], ), PricingMatrixRow( criteria=criteria32, cells=[ - PricingMatrixCell(criteria=criteria21, value=None), - PricingMatrixCell(criteria=criteria22, value=None), - PricingMatrixCell(criteria=criteria23, value=None), + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=None), ], ), PricingMatrixRow( criteria=criteria33, cells=[ - PricingMatrixCell(criteria=criteria21, value=None), - PricingMatrixCell(criteria=criteria22, value=223), - PricingMatrixCell(criteria=criteria23, value=None), + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=223), ], ), PricingMatrixRow( criteria=criteria34, cells=[ - PricingMatrixCell(criteria=criteria21, value=None), - PricingMatrixCell(criteria=criteria22, value=None), - PricingMatrixCell(criteria=criteria23, value=None), + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=None), + ], + ), + ], + ), + PricingMatrix( + criteria=criteria23, + rows=[ + PricingMatrixRow( + criteria=criteria31, + cells=[ + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=None), + ], + ), + PricingMatrixRow( + criteria=criteria32, + cells=[ + PricingMatrixCell(criteria=criteria11, value=132), + PricingMatrixCell(criteria=criteria12, value=None), + ], + ), + PricingMatrixRow( + criteria=criteria33, + cells=[ + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=None), + ], + ), + PricingMatrixRow( + criteria=criteria34, + cells=[ + PricingMatrixCell(criteria=criteria11, value=None), + PricingMatrixCell(criteria=criteria12, value=None), ], ), ],