From e82d6924c2d5c3edec1932820ed066929925fe06 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 23 May 2022 09:57:22 +0200
Subject: [PATCH 1/3] misc: split pricing manager tests (#65459)

---
 tests/pricing/manager/__init__.py             |   0
 .../test_agenda.py}                           | 727 +-----------------
 tests/pricing/manager/test_criteria.py        | 186 +++++
 tests/pricing/manager/test_import_export.py   | 235 ++++++
 tests/pricing/manager/test_pricing.py         | 324 ++++++++
 5 files changed, 746 insertions(+), 726 deletions(-)
 create mode 100644 tests/pricing/manager/__init__.py
 rename tests/pricing/{test_manager.py => manager/test_agenda.py} (51%)
 create mode 100644 tests/pricing/manager/test_criteria.py
 create mode 100644 tests/pricing/manager/test_import_export.py
 create mode 100644 tests/pricing/manager/test_pricing.py

diff --git a/tests/pricing/manager/__init__.py b/tests/pricing/manager/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/pricing/test_manager.py b/tests/pricing/manager/test_agenda.py
similarity index 51%
rename from tests/pricing/test_manager.py
rename to tests/pricing/manager/test_agenda.py
index d9961d7..85a29c3 100644
--- a/tests/pricing/test_manager.py
+++ b/tests/pricing/manager/test_agenda.py
@@ -1,691 +1,15 @@
-import copy
 import datetime
-import json
 from unittest import mock
 
 import pytest
-from webtest import Upload
 
 from lingo.agendas.models import Agenda
-from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
+from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing
 from tests.utils import login
 
 pytestmark = pytest.mark.django_db
 
 
-def test_export_site(freezer, app, admin_user):
-    freezer.move_to('2020-06-15')
-    login(app)
-    resp = app.get('/manage/pricing/')
-    resp = resp.click('Export')
-
-    resp = resp.form.submit()
-    assert resp.headers['content-type'] == 'application/json'
-    assert resp.headers['content-disposition'] == 'attachment; filename="export_pricing_config_20200615.json"'
-
-    site_json = json.loads(resp.text)
-    assert site_json == {
-        'agendas': [],
-        'pricing_categories': [],
-        'pricing_models': [],
-    }
-
-    Agenda.objects.create(label='Foo Bar')
-    resp = app.get('/manage/pricing/export/')
-    resp = resp.form.submit()
-
-    site_json = json.loads(resp.text)
-    assert len(site_json['agendas']) == 1
-
-    resp = app.get('/manage/pricing/export/')
-    resp.form['agendas'] = False
-    resp.form['pricing_categories'] = False
-    resp.form['pricing_models'] = False
-    resp = resp.form.submit()
-
-    site_json = json.loads(resp.text)
-    assert 'agendas' not in site_json
-    assert 'pricing_categories' not in site_json
-    assert 'pricing_models' not in site_json
-
-
-def test_add_pricing(app, admin_user):
-    app = login(app)
-    resp = app.get('/manage/')
-    resp = resp.click('Pricing')
-    resp = resp.click('New pricing model')
-    resp.form['label'] = 'Pricing model for lunch'
-    resp = resp.form.submit()
-    pricing = Pricing.objects.latest('pk')
-    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
-    assert pricing.label == 'Pricing model for lunch'
-    assert pricing.slug == 'pricing-model-for-lunch'
-
-
-def test_detail_pricing(app, admin_user):
-    pricing = Pricing.objects.create(label='Model')
-
-    app = login(app)
-    resp = app.get('/manage/pricing/')
-    resp = resp.click(href='/manage/pricing/%s/' % pricing.pk)
-    assert '/manage/pricing/%s/edit/' % pricing.pk in resp
-    assert '/manage/pricing/%s/delete/' % pricing.pk in resp
-
-
-def test_edit_pricing(app, admin_user):
-    pricing = Pricing.objects.create(label='Model 1')
-    pricing2 = Pricing.objects.create(label='Model 2')
-
-    app = login(app)
-    resp = app.get('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.click(href='/manage/pricing/%s/edit/' % pricing.pk)
-    resp.form['label'] = 'Model Foo'
-    resp.form['slug'] = pricing2.slug
-    resp = resp.form.submit()
-    assert resp.context['form'].errors['slug'] == ['Pricing with this Identifier already exists.']
-
-    resp.form['slug'] = 'foo-bar'
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
-    pricing.refresh_from_db()
-    assert pricing.label == 'Model Foo'
-    assert pricing.slug == 'foo-bar'
-
-
-def test_delete_pricing(app, admin_user):
-    pricing = Pricing.objects.create(label='Model')
-
-    app = login(app)
-    resp = app.get('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.click(href='/manage/pricing/%s/delete/' % pricing.pk)
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/')
-    assert Pricing.objects.exists() is False
-
-
-def test_duplicate_pricing(app, admin_user):
-    pricing = Pricing.objects.create(label='Model')
-    assert Pricing.objects.count() == 1
-
-    app = login(app)
-    resp = app.get('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.click(href='/manage/pricing/%s/duplicate/' % pricing.pk)
-    resp = resp.form.submit()
-    assert Pricing.objects.count() == 2
-
-    new_pricing = Pricing.objects.latest('pk')
-    assert resp.location == '/manage/pricing/%s/' % new_pricing.pk
-    assert new_pricing.pk != pricing.pk
-
-    resp = resp.follow()
-    assert 'copy-of-model' in resp.text
-
-    resp = resp.click(href='/manage/pricing/%s/duplicate/' % new_pricing.pk)
-    resp.form['label'] = 'hop'
-    resp = resp.form.submit().follow()
-    assert 'hop' in resp.text
-
-
-@pytest.mark.freeze_time('2021-07-08')
-def test_import_pricing(app, admin_user):
-    pricing = Pricing.objects.create(label='Model')
-
-    app = login(app)
-    resp = app.get('/manage/pricing/%s/export/' % pricing.id)
-    assert resp.headers['content-type'] == 'application/json'
-    assert resp.headers['content-disposition'] == 'attachment; filename="export_pricing_model_20210708.json"'
-    pricing_export = resp.text
-
-    # existing pricing
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.follow()
-    assert 'No pricing model created. A pricing model has been updated.' not in resp.text
-    assert Pricing.objects.count() == 1
-
-    # new pricing
-    Pricing.objects.all().delete()
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
-    resp = resp.form.submit()
-    pricing = Pricing.objects.latest('pk')
-    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.follow()
-    assert 'A pricing model has been created. No pricing model updated.' not in resp.text
-    assert Pricing.objects.count() == 1
-
-    # multiple pricing
-    pricings = json.loads(pricing_export)
-    pricings['pricing_models'].append(copy.copy(pricings['pricing_models'][0]))
-    pricings['pricing_models'].append(copy.copy(pricings['pricing_models'][0]))
-    pricings['pricing_models'][1]['label'] = 'Foo bar 2'
-    pricings['pricing_models'][1]['slug'] = 'foo-bar-2'
-    pricings['pricing_models'][2]['label'] = 'Foo bar 3'
-    pricings['pricing_models'][2]['slug'] = 'foo-bar-3'
-
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', json.dumps(pricings).encode('utf-8'), 'application/json')
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/')
-    resp = resp.follow()
-    assert '2 pricing models have been created. A pricing model has been updated.' in resp.text
-    assert Pricing.objects.count() == 3
-
-    Pricing.objects.all().delete()
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', json.dumps(pricings).encode('utf-8'), 'application/json')
-    resp = resp.form.submit().follow()
-    assert '3 pricing models have been created. No pricing model updated.' in resp.text
-    assert Pricing.objects.count() == 3
-
-
-def test_pricing_edit_extra_variables(app, admin_user):
-    pricing = Pricing.objects.create(label='Model')
-    assert pricing.extra_variables == {}
-
-    app = login(app)
-    resp = app.get('/manage/pricing/%s/' % pricing.id)
-    assert '<label>Extra variables:</label>' not in resp.text
-    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
-    resp.form['form-0-key'] = 'foo'
-    resp.form['form-0-value'] = 'bar'
-    resp = resp.form.submit().follow()
-    pricing.refresh_from_db()
-    assert pricing.extra_variables == {'foo': 'bar'}
-    assert '<label>Extra variables:</label>' in resp.text
-    assert '<i>foo</i>' in resp
-
-    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
-    assert resp.form['form-TOTAL_FORMS'].value == '2'
-    assert resp.form['form-0-key'].value == 'foo'
-    assert resp.form['form-0-value'].value == 'bar'
-    assert resp.form['form-1-key'].value == ''
-    assert resp.form['form-1-value'].value == ''
-    resp.form['form-0-value'] = 'bar-bis'
-    resp.form['form-1-key'] = 'blah'
-    resp.form['form-1-value'] = 'baz'
-    resp = resp.form.submit().follow()
-    pricing.refresh_from_db()
-    assert pricing.extra_variables == {
-        'foo': 'bar-bis',
-        'blah': 'baz',
-    }
-    assert '<i>blah</i>, <i>foo</i>' in resp
-
-    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
-    assert resp.form['form-TOTAL_FORMS'].value == '3'
-    assert resp.form['form-0-key'].value == 'blah'
-    assert resp.form['form-0-value'].value == 'baz'
-    assert resp.form['form-1-key'].value == 'foo'
-    assert resp.form['form-1-value'].value == 'bar-bis'
-    assert resp.form['form-2-key'].value == ''
-    assert resp.form['form-2-value'].value == ''
-    resp.form['form-1-key'] = 'foo'
-    resp.form['form-1-value'] = 'bar'
-    resp.form['form-0-key'] = ''
-    resp = resp.form.submit().follow()
-    pricing.refresh_from_db()
-    assert pricing.extra_variables == {
-        'foo': 'bar',
-    }
-    assert '<i>foo</i>' in resp
-
-
-def test_pricing_add_category(app, admin_user):
-    pricing = Pricing.objects.create(label='Model')
-    category1 = CriteriaCategory.objects.create(label='Cat 1')
-    category2 = CriteriaCategory.objects.create(label='Cat 2')
-    category3 = CriteriaCategory.objects.create(label='Cat 3')
-    category4 = CriteriaCategory.objects.create(label='Cat 4')
-
-    app = login(app)
-    resp = app.get('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.click(href='/manage/pricing/%s/category/add/' % pricing.pk)
-    assert list(resp.context['form'].fields['category'].queryset) == [
-        category1,
-        category2,
-        category3,
-        category4,
-    ]
-    resp.form['category'] = category1.pk
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.follow()
-    assert '/manage/pricing/%s/category/add/' % pricing.pk in resp
-    assert list(
-        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
-    ) == [category1.pk]
-    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
-        1
-    ]
-
-    resp = app.get('/manage/pricing/%s/category/add/' % pricing.pk)
-    assert list(resp.context['form'].fields['category'].queryset) == [category2, category3, category4]
-    resp.form['category'] = category4.pk
-    resp = resp.form.submit().follow()
-    assert list(
-        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
-    ) == [category1.pk, category4.pk]
-    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
-        1,
-        2,
-    ]
-
-    resp = app.get('/manage/pricing/%s/category/add/' % pricing.pk)
-    assert list(resp.context['form'].fields['category'].queryset) == [category2, category3]
-    resp.form['category'] = category2.pk
-    resp = resp.form.submit().follow()
-    assert '/manage/pricing/%s/category/add/' % pricing.pk not in resp
-    assert list(
-        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
-    ) == [category1.pk, category4.pk, category2.pk]
-    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
-        1,
-        2,
-        3,
-    ]
-
-    app.get('/manage/pricing/%s/category/add/' % pricing.pk, status=404)
-
-
-def test_pricing_edit_category(app, admin_user):
-    category1 = CriteriaCategory.objects.create(label='Cat 1')
-    criteria1 = Criteria.objects.create(label='Crit 1', category=category1)
-    criteria2 = Criteria.objects.create(label='Crit 2', category=category1)
-    criteria3 = Criteria.objects.create(label='Crit 3', category=category1)
-    criteria4 = Criteria.objects.create(label='Crit 4', category=category1)
-    category2 = CriteriaCategory.objects.create(label='cat 2')
-    criteria5 = Criteria.objects.create(label='Crit 5', category=category2)
-    pricing = Pricing.objects.create(label='Model')
-    pricing.categories.add(category1, through_defaults={'order': 1})
-    pricing.categories.add(category2, through_defaults={'order': 2})
-
-    app = login(app)
-    resp = app.get('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.click(href='/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category1.pk))
-    assert list(resp.context['form'].fields['criterias'].queryset) == [
-        criteria1,
-        criteria2,
-        criteria3,
-        criteria4,
-    ]
-    assert list(resp.context['form'].initial['criterias']) == []
-    resp.form['criterias'] = [criteria1.pk, criteria3.pk]
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.follow()
-    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria3]
-
-    resp = app.get('/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category1.pk))
-    assert list(resp.context['form'].initial['criterias']) == [criteria1, criteria3]
-    resp.form['criterias'] = [criteria1.pk, criteria4.pk]
-    resp = resp.form.submit().follow()
-    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria4]
-
-    resp = app.get('/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category2.pk))
-    assert list(resp.context['form'].fields['criterias'].queryset) == [criteria5]
-    assert list(resp.context['form'].initial['criterias']) == []
-    resp.form['criterias'] = [criteria5.pk]
-    resp = resp.form.submit().follow()
-    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria4, criteria5]
-
-
-def test_pricing_delete_category(app, admin_user):
-    category1 = CriteriaCategory.objects.create(label='Cat 1')
-    criteria1 = Criteria.objects.create(label='Crit 1', category=category1)
-    category2 = CriteriaCategory.objects.create(label='Cat 2')
-    criteria2 = Criteria.objects.create(label='Crit 2', category=category2)
-    pricing = Pricing.objects.create(label='Model')
-    pricing.categories.add(category1, through_defaults={'order': 1})
-    pricing.categories.add(category2, through_defaults={'order': 2})
-    pricing.criterias.add(criteria1, criteria2)
-
-    app = login(app)
-    resp = app.get('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.click(href='/manage/pricing/%s/category/%s/delete/' % (pricing.pk, category1.pk))
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
-    resp = resp.follow()
-    assert list(pricing.categories.all()) == [category2]
-    assert list(pricing.criterias.all()) == [criteria2]
-
-    # not linked
-    app.get('/manage/pricing/%s/category/%s/delete/' % (pricing.pk, category1.pk), status=404)
-    # unknown
-    app.get('/manage/pricing/%s/category/%s/delete/' % (pricing.pk, 0), status=404)
-
-
-def test_pricing_reorder_categories(app, admin_user):
-    category1 = CriteriaCategory.objects.create(label='Cat 1')
-    category2 = CriteriaCategory.objects.create(label='Cat 2')
-    category3 = CriteriaCategory.objects.create(label='Cat 3')
-    category4 = CriteriaCategory.objects.create(label='Cat 4')
-    pricing = Pricing.objects.create(label='Model')
-    pricing.categories.add(category1, through_defaults={'order': 1})
-    pricing.categories.add(category2, through_defaults={'order': 2})
-    pricing.categories.add(category3, through_defaults={'order': 3})
-    assert list(
-        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
-    ) == [category1.pk, category2.pk, category3.pk]
-    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
-        1,
-        2,
-        3,
-    ]
-
-    app = login(app)
-    # missing get params
-    app.get('/manage/pricing/%s/order/' % (pricing.pk), status=400)
-
-    # bad new-order param
-    bad_params = [
-        # missing category3 in order
-        ','.join(str(x) for x in [category1.pk, category2.pk]),
-        # category1 mentionned twice
-        ','.join(str(x) for x in [category1.pk, category2.pk, category3.pk, category1.pk]),
-        # category4 not in pricing categories
-        ','.join(str(x) for x in [category1.pk, category2.pk, category3.pk, category4.pk]),
-        # not an id
-        'foo,1,2,3',
-        ' 1 ,2,3',
-    ]
-    for bad_param in bad_params:
-        app.get(
-            '/manage/pricing/%s/order/' % (pricing.pk),
-            params={'new-order': bad_param},
-            status=400,
-        )
-    # not changed
-    assert list(
-        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
-    ) == [category1.pk, category2.pk, category3.pk]
-    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
-        1,
-        2,
-        3,
-    ]
-
-    # change order
-    app.get(
-        '/manage/pricing/%s/order/' % (pricing.pk),
-        params={'new-order': ','.join(str(x) for x in [category3.pk, category1.pk, category2.pk])},
-    )
-    assert list(
-        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
-    ) == [category3.pk, category1.pk, category2.pk]
-    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
-        1,
-        2,
-        3,
-    ]
-
-
-def test_add_category(app, admin_user):
-    app = login(app)
-    resp = app.get('/manage/')
-    resp = resp.click('Pricing')
-    resp = resp.click('Criterias')
-    resp = resp.click('New category')
-    resp.form['label'] = 'QF'
-    resp = resp.form.submit()
-    category = CriteriaCategory.objects.latest('pk')
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    assert category.label == 'QF'
-    assert category.slug == 'qf'
-
-
-def test_edit_category(app, admin_user):
-    category = CriteriaCategory.objects.create(label='QF')
-    category2 = CriteriaCategory.objects.create(label='Domicile')
-
-    app = login(app)
-    resp = app.get('/manage/pricing/criterias/')
-    resp = resp.click(href='/manage/pricing/criteria/category/%s/edit/' % category.pk)
-    resp.form['label'] = 'QF Foo'
-    resp.form['slug'] = category2.slug
-    resp = resp.form.submit()
-    assert resp.context['form'].errors['slug'] == ['Criteria category with this Identifier already exists.']
-
-    resp.form['slug'] = 'baz2'
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    category.refresh_from_db()
-    assert category.label == 'QF Foo'
-    assert category.slug == 'baz2'
-
-
-def test_delete_category(app, admin_user):
-    category = CriteriaCategory.objects.create(label='QF')
-    Criteria.objects.create(label='QF 1', category=category)
-
-    app = login(app)
-    resp = app.get('/manage/pricing/criterias/')
-    resp = resp.click(href='/manage/pricing/criteria/category/%s/delete/' % category.pk)
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    assert CriteriaCategory.objects.exists() is False
-    assert Criteria.objects.exists() is False
-
-
-def test_add_criteria(app, admin_user):
-    category = CriteriaCategory.objects.create(label='QF')
-
-    app = login(app)
-    resp = app.get('/manage/pricing/criterias/')
-    resp = resp.click('Add a criteria')
-    resp.form['label'] = 'QF < 1'
-    resp.form['condition'] = 'qf < 1 #'
-    assert 'slug' not in resp.context['form'].fields
-    resp = resp.form.submit()
-    assert resp.context['form'].errors['condition'] == ['Invalid syntax.']
-    resp.form['condition'] = 'qf < 1'
-    resp = resp.form.submit()
-    criteria = Criteria.objects.latest('pk')
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    assert criteria.label == 'QF < 1'
-    assert criteria.category == category
-    assert criteria.slug == 'qf-1'
-    assert criteria.condition == 'qf < 1'
-    assert criteria.order == 1
-
-    resp = app.get('/manage/pricing/criteria/category/%s/add/' % category.pk)
-    resp.form['label'] = 'QF < 1'
-    resp.form['condition'] = 'qf < 1'
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    criteria = Criteria.objects.latest('pk')
-    assert criteria.label == 'QF < 1'
-    assert criteria.category == category
-    assert criteria.slug == 'qf-1-1'
-    assert criteria.condition == 'qf < 1'
-    assert criteria.order == 2
-
-
-def test_edit_criteria(app, admin_user):
-    category = CriteriaCategory.objects.create(label='QF')
-    criteria = Criteria.objects.create(label='QF 1', category=category)
-    criteria2 = Criteria.objects.create(label='QF 2', category=category)
-    category2 = CriteriaCategory.objects.create(label='Foo')
-    criteria3 = Criteria.objects.create(label='foo-bar', category=category2)
-
-    app = login(app)
-    resp = app.get('/manage/pricing/criterias/')
-    resp = resp.click(href='/manage/pricing/criteria/category/%s/%s/edit/' % (category.pk, criteria.pk))
-    resp.form['label'] = 'QF 1 bis'
-    resp.form['slug'] = criteria2.slug
-    resp.form['condition'] = 'qf <= 1 #'
-    resp = resp.form.submit()
-    assert resp.context['form'].errors['slug'] == ['Another criteria exists with the same identifier.']
-    assert resp.context['form'].errors['condition'] == ['Invalid syntax.']
-
-    resp.form['condition'] = 'qf <= 1'
-    resp.form['slug'] = criteria3.slug
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    criteria.refresh_from_db()
-    assert criteria.label == 'QF 1 bis'
-    assert criteria.slug == 'foo-bar'
-    assert criteria.condition == 'qf <= 1'
-
-
-def test_delete_criteria(app, admin_user):
-    category = CriteriaCategory.objects.create(label='QF')
-    criteria = Criteria.objects.create(label='QF 1', category=category)
-
-    app = login(app)
-    resp = app.get('/manage/pricing/criterias/')
-    resp = resp.click(href='/manage/pricing/criteria/category/%s/%s/delete/' % (category.pk, criteria.pk))
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    assert CriteriaCategory.objects.exists() is True
-    assert Criteria.objects.exists() is False
-
-
-def test_reorder_criterias(app, admin_user):
-    category = CriteriaCategory.objects.create(label='QF')
-    criteria1 = Criteria.objects.create(label='QF 1', category=category)
-    criteria2 = Criteria.objects.create(label='QF 2', category=category)
-    criteria3 = Criteria.objects.create(label='QF 3', category=category)
-    criteria4 = Criteria.objects.create(label='QF 4', category=category)
-    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
-        criteria1.pk,
-        criteria2.pk,
-        criteria3.pk,
-        criteria4.pk,
-    ]
-    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
-
-    app = login(app)
-    # missing get params
-    app.get('/manage/pricing/criteria/category/%s/order/' % (category.pk), status=400)
-
-    # bad new-order param
-    bad_params = [
-        # missing criteria3 in order
-        ','.join(str(x) for x in [criteria1.pk, criteria2.pk, criteria4.pk]),
-        # criteria1 mentionned twice
-        ','.join(str(x) for x in [criteria1.pk, criteria2.pk, criteria3.pk, criteria4.pk, criteria1.pk]),
-        # not an id
-        'foo,1,2,3,4',
-        ' 1 ,2,3,4',
-    ]
-    for bad_param in bad_params:
-        app.get(
-            '/manage/pricing/criteria/category/%s/order/' % (category.pk),
-            params={'new-order': bad_param},
-            status=400,
-        )
-    # not changed
-    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
-        criteria1.pk,
-        criteria2.pk,
-        criteria3.pk,
-        criteria4.pk,
-    ]
-    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
-
-    # change order
-    app.get(
-        '/manage/pricing/criteria/category/%s/order/' % (category.pk),
-        params={
-            'new-order': ','.join(str(x) for x in [criteria3.pk, criteria1.pk, criteria4.pk, criteria2.pk])
-        },
-    )
-    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
-        criteria3.pk,
-        criteria1.pk,
-        criteria4.pk,
-        criteria2.pk,
-    ]
-    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
-
-
-@pytest.mark.freeze_time('2021-07-08')
-def test_import_criteria_category(app, admin_user):
-    category = CriteriaCategory.objects.create(label='Foo bar')
-    Criteria.objects.create(label='Foo', category=category)
-    Criteria.objects.create(label='Baz', category=category)
-
-    app = login(app)
-    resp = app.get('/manage/pricing/criteria/category/%s/export/' % category.id)
-    assert resp.headers['content-type'] == 'application/json'
-    assert (
-        resp.headers['content-disposition']
-        == 'attachment; filename="export_pricing_category_foo-bar_20210708.json"'
-    )
-    category_export = resp.text
-
-    # existing category
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    resp = resp.follow()
-    assert (
-        'No pricing criteria category created. A pricing criteria category has been updated.' not in resp.text
-    )
-    assert CriteriaCategory.objects.count() == 1
-    assert Criteria.objects.count() == 2
-
-    # new category
-    CriteriaCategory.objects.all().delete()
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/criterias/')
-    resp = resp.follow()
-    assert (
-        'A pricing criteria category has been created. No pricing criteria category updated.' not in resp.text
-    )
-    assert CriteriaCategory.objects.count() == 1
-    assert Criteria.objects.count() == 2
-
-    # multiple categories
-    categories = json.loads(category_export)
-    categories['pricing_categories'].append(copy.copy(categories['pricing_categories'][0]))
-    categories['pricing_categories'].append(copy.copy(categories['pricing_categories'][0]))
-    categories['pricing_categories'][1]['label'] = 'Foo bar 2'
-    categories['pricing_categories'][1]['slug'] = 'foo-bar-2'
-    categories['pricing_categories'][2]['label'] = 'Foo bar 3'
-    categories['pricing_categories'][2]['slug'] = 'foo-bar-3'
-
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload(
-        'export.json', json.dumps(categories).encode('utf-8'), 'application/json'
-    )
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/')
-    resp = resp.follow()
-    assert (
-        '2 pricing criteria categories have been created. A pricing criteria category has been updated.'
-        in resp.text
-    )
-    assert CriteriaCategory.objects.count() == 3
-    assert Criteria.objects.count() == 6
-
-    CriteriaCategory.objects.all().delete()
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload(
-        'export.json', json.dumps(categories).encode('utf-8'), 'application/json'
-    )
-    resp = resp.form.submit().follow()
-    assert (
-        '3 pricing criteria categories have been created. No pricing criteria category updated.' in resp.text
-    )
-    assert CriteriaCategory.objects.count() == 3
-    assert Criteria.objects.count() == 6
-
-
 @mock.patch('lingo.pricing.views.refresh_agendas')
 def test_refresh_agendas(mock_refresh, app, admin_user):
     app = login(app)
@@ -697,55 +21,6 @@ def test_refresh_agendas(mock_refresh, app, admin_user):
     assert mock_refresh.call_args_list == [mock.call()]
 
 
-@pytest.mark.freeze_time('2021-07-08')
-def test_import_agenda(app, admin_user):
-    agenda = Agenda.objects.create(label='Foo Bar')
-
-    app = login(app)
-    resp = app.get('/manage/pricing/agenda/%s/' % agenda.pk)
-    resp = resp.click('Export')
-    assert resp.headers['content-type'] == 'application/json'
-    assert (
-        resp.headers['content-disposition']
-        == 'attachment; filename="export_pricing_agenda_foo-bar_20210708.json"'
-    )
-    agenda_export = resp.text
-
-    # existing agenda
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', agenda_export.encode('utf-8'), 'application/json')
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/agenda/%s/' % agenda.pk)
-    resp = resp.follow()
-    assert 'An agenda has been updated.' not in resp.text
-    assert Agenda.objects.count() == 1
-
-    # unknown agenda
-    Agenda.objects.all().delete()
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', agenda_export.encode('utf-8'), 'application/json')
-    resp = resp.form.submit()
-    assert resp.context['form'].errors['config_json'] == ['Missing "foo-bar" agenda']
-
-    # multiple pricing
-    Agenda.objects.create(label='Foo Bar')
-    Agenda.objects.create(label='Foo Bar 2')
-    agendas = json.loads(agenda_export)
-    agendas['agendas'].append(copy.copy(agendas['agendas'][0]))
-    agendas['agendas'][1]['slug'] = 'foo-bar-2'
-
-    resp = app.get('/manage/pricing/', status=200)
-    resp = resp.click('Import')
-    resp.form['config_json'] = Upload('export.json', json.dumps(agendas).encode('utf-8'), 'application/json')
-    resp = resp.form.submit()
-    assert resp.location.endswith('/manage/pricing/')
-    resp = resp.follow()
-    assert '2 agendas have been updated.' in resp.text
-    assert Agenda.objects.count() == 2
-
-
 def test_add_agenda_pricing(app, admin_user):
     agenda = Agenda.objects.create(label='Foo Bar')
     pricing = Pricing.objects.create(label='Model')
diff --git a/tests/pricing/manager/test_criteria.py b/tests/pricing/manager/test_criteria.py
new file mode 100644
index 0000000..a1e99f0
--- /dev/null
+++ b/tests/pricing/manager/test_criteria.py
@@ -0,0 +1,186 @@
+import pytest
+
+from lingo.pricing.models import Criteria, CriteriaCategory
+from tests.utils import login
+
+pytestmark = pytest.mark.django_db
+
+
+def test_add_category(app, admin_user):
+    app = login(app)
+    resp = app.get('/manage/')
+    resp = resp.click('Pricing')
+    resp = resp.click('Criterias')
+    resp = resp.click('New category')
+    resp.form['label'] = 'QF'
+    resp = resp.form.submit()
+    category = CriteriaCategory.objects.latest('pk')
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    assert category.label == 'QF'
+    assert category.slug == 'qf'
+
+
+def test_edit_category(app, admin_user):
+    category = CriteriaCategory.objects.create(label='QF')
+    category2 = CriteriaCategory.objects.create(label='Domicile')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/criterias/')
+    resp = resp.click(href='/manage/pricing/criteria/category/%s/edit/' % category.pk)
+    resp.form['label'] = 'QF Foo'
+    resp.form['slug'] = category2.slug
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['slug'] == ['Criteria category with this Identifier already exists.']
+
+    resp.form['slug'] = 'baz2'
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    category.refresh_from_db()
+    assert category.label == 'QF Foo'
+    assert category.slug == 'baz2'
+
+
+def test_delete_category(app, admin_user):
+    category = CriteriaCategory.objects.create(label='QF')
+    Criteria.objects.create(label='QF 1', category=category)
+
+    app = login(app)
+    resp = app.get('/manage/pricing/criterias/')
+    resp = resp.click(href='/manage/pricing/criteria/category/%s/delete/' % category.pk)
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    assert CriteriaCategory.objects.exists() is False
+    assert Criteria.objects.exists() is False
+
+
+def test_add_criteria(app, admin_user):
+    category = CriteriaCategory.objects.create(label='QF')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/criterias/')
+    resp = resp.click('Add a criteria')
+    resp.form['label'] = 'QF < 1'
+    resp.form['condition'] = 'qf < 1 #'
+    assert 'slug' not in resp.context['form'].fields
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['condition'] == ['Invalid syntax.']
+    resp.form['condition'] = 'qf < 1'
+    resp = resp.form.submit()
+    criteria = Criteria.objects.latest('pk')
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    assert criteria.label == 'QF < 1'
+    assert criteria.category == category
+    assert criteria.slug == 'qf-1'
+    assert criteria.condition == 'qf < 1'
+    assert criteria.order == 1
+
+    resp = app.get('/manage/pricing/criteria/category/%s/add/' % category.pk)
+    resp.form['label'] = 'QF < 1'
+    resp.form['condition'] = 'qf < 1'
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    criteria = Criteria.objects.latest('pk')
+    assert criteria.label == 'QF < 1'
+    assert criteria.category == category
+    assert criteria.slug == 'qf-1-1'
+    assert criteria.condition == 'qf < 1'
+    assert criteria.order == 2
+
+
+def test_edit_criteria(app, admin_user):
+    category = CriteriaCategory.objects.create(label='QF')
+    criteria = Criteria.objects.create(label='QF 1', category=category)
+    criteria2 = Criteria.objects.create(label='QF 2', category=category)
+    category2 = CriteriaCategory.objects.create(label='Foo')
+    criteria3 = Criteria.objects.create(label='foo-bar', category=category2)
+
+    app = login(app)
+    resp = app.get('/manage/pricing/criterias/')
+    resp = resp.click(href='/manage/pricing/criteria/category/%s/%s/edit/' % (category.pk, criteria.pk))
+    resp.form['label'] = 'QF 1 bis'
+    resp.form['slug'] = criteria2.slug
+    resp.form['condition'] = 'qf <= 1 #'
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['slug'] == ['Another criteria exists with the same identifier.']
+    assert resp.context['form'].errors['condition'] == ['Invalid syntax.']
+
+    resp.form['condition'] = 'qf <= 1'
+    resp.form['slug'] = criteria3.slug
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    criteria.refresh_from_db()
+    assert criteria.label == 'QF 1 bis'
+    assert criteria.slug == 'foo-bar'
+    assert criteria.condition == 'qf <= 1'
+
+
+def test_delete_criteria(app, admin_user):
+    category = CriteriaCategory.objects.create(label='QF')
+    criteria = Criteria.objects.create(label='QF 1', category=category)
+
+    app = login(app)
+    resp = app.get('/manage/pricing/criterias/')
+    resp = resp.click(href='/manage/pricing/criteria/category/%s/%s/delete/' % (category.pk, criteria.pk))
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    assert CriteriaCategory.objects.exists() is True
+    assert Criteria.objects.exists() is False
+
+
+def test_reorder_criterias(app, admin_user):
+    category = CriteriaCategory.objects.create(label='QF')
+    criteria1 = Criteria.objects.create(label='QF 1', category=category)
+    criteria2 = Criteria.objects.create(label='QF 2', category=category)
+    criteria3 = Criteria.objects.create(label='QF 3', category=category)
+    criteria4 = Criteria.objects.create(label='QF 4', category=category)
+    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
+        criteria1.pk,
+        criteria2.pk,
+        criteria3.pk,
+        criteria4.pk,
+    ]
+    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
+
+    app = login(app)
+    # missing get params
+    app.get('/manage/pricing/criteria/category/%s/order/' % (category.pk), status=400)
+
+    # bad new-order param
+    bad_params = [
+        # missing criteria3 in order
+        ','.join(str(x) for x in [criteria1.pk, criteria2.pk, criteria4.pk]),
+        # criteria1 mentionned twice
+        ','.join(str(x) for x in [criteria1.pk, criteria2.pk, criteria3.pk, criteria4.pk, criteria1.pk]),
+        # not an id
+        'foo,1,2,3,4',
+        ' 1 ,2,3,4',
+    ]
+    for bad_param in bad_params:
+        app.get(
+            '/manage/pricing/criteria/category/%s/order/' % (category.pk),
+            params={'new-order': bad_param},
+            status=400,
+        )
+    # not changed
+    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
+        criteria1.pk,
+        criteria2.pk,
+        criteria3.pk,
+        criteria4.pk,
+    ]
+    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
+
+    # change order
+    app.get(
+        '/manage/pricing/criteria/category/%s/order/' % (category.pk),
+        params={
+            'new-order': ','.join(str(x) for x in [criteria3.pk, criteria1.pk, criteria4.pk, criteria2.pk])
+        },
+    )
+    assert list(category.criterias.values_list('pk', flat=True).order_by('order')) == [
+        criteria3.pk,
+        criteria1.pk,
+        criteria4.pk,
+        criteria2.pk,
+    ]
+    assert list(category.criterias.values_list('order', flat=True).order_by('order')) == [1, 2, 3, 4]
diff --git a/tests/pricing/manager/test_import_export.py b/tests/pricing/manager/test_import_export.py
new file mode 100644
index 0000000..c631cbf
--- /dev/null
+++ b/tests/pricing/manager/test_import_export.py
@@ -0,0 +1,235 @@
+import copy
+import json
+
+import pytest
+from webtest import Upload
+
+from lingo.agendas.models import Agenda
+from lingo.pricing.models import Criteria, CriteriaCategory, Pricing
+from tests.utils import login
+
+pytestmark = pytest.mark.django_db
+
+
+def test_export_site(freezer, app, admin_user):
+    freezer.move_to('2020-06-15')
+    login(app)
+    resp = app.get('/manage/pricing/')
+    resp = resp.click('Export')
+
+    resp = resp.form.submit()
+    assert resp.headers['content-type'] == 'application/json'
+    assert resp.headers['content-disposition'] == 'attachment; filename="export_pricing_config_20200615.json"'
+
+    site_json = json.loads(resp.text)
+    assert site_json == {
+        'agendas': [],
+        'pricing_categories': [],
+        'pricing_models': [],
+    }
+
+    Agenda.objects.create(label='Foo Bar')
+    resp = app.get('/manage/pricing/export/')
+    resp = resp.form.submit()
+
+    site_json = json.loads(resp.text)
+    assert len(site_json['agendas']) == 1
+
+    resp = app.get('/manage/pricing/export/')
+    resp.form['agendas'] = False
+    resp.form['pricing_categories'] = False
+    resp.form['pricing_models'] = False
+    resp = resp.form.submit()
+
+    site_json = json.loads(resp.text)
+    assert 'agendas' not in site_json
+    assert 'pricing_categories' not in site_json
+    assert 'pricing_models' not in site_json
+
+
+@pytest.mark.freeze_time('2021-07-08')
+def test_import_pricing(app, admin_user):
+    pricing = Pricing.objects.create(label='Model')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/%s/export/' % pricing.id)
+    assert resp.headers['content-type'] == 'application/json'
+    assert resp.headers['content-disposition'] == 'attachment; filename="export_pricing_model_20210708.json"'
+    pricing_export = resp.text
+
+    # existing pricing
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.follow()
+    assert 'No pricing model created. A pricing model has been updated.' not in resp.text
+    assert Pricing.objects.count() == 1
+
+    # new pricing
+    Pricing.objects.all().delete()
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
+    resp = resp.form.submit()
+    pricing = Pricing.objects.latest('pk')
+    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.follow()
+    assert 'A pricing model has been created. No pricing model updated.' not in resp.text
+    assert Pricing.objects.count() == 1
+
+    # multiple pricing
+    pricings = json.loads(pricing_export)
+    pricings['pricing_models'].append(copy.copy(pricings['pricing_models'][0]))
+    pricings['pricing_models'].append(copy.copy(pricings['pricing_models'][0]))
+    pricings['pricing_models'][1]['label'] = 'Foo bar 2'
+    pricings['pricing_models'][1]['slug'] = 'foo-bar-2'
+    pricings['pricing_models'][2]['label'] = 'Foo bar 3'
+    pricings['pricing_models'][2]['slug'] = 'foo-bar-3'
+
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', json.dumps(pricings).encode('utf-8'), 'application/json')
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/')
+    resp = resp.follow()
+    assert '2 pricing models have been created. A pricing model has been updated.' in resp.text
+    assert Pricing.objects.count() == 3
+
+    Pricing.objects.all().delete()
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', json.dumps(pricings).encode('utf-8'), 'application/json')
+    resp = resp.form.submit().follow()
+    assert '3 pricing models have been created. No pricing model updated.' in resp.text
+    assert Pricing.objects.count() == 3
+
+
+@pytest.mark.freeze_time('2021-07-08')
+def test_import_criteria_category(app, admin_user):
+    category = CriteriaCategory.objects.create(label='Foo bar')
+    Criteria.objects.create(label='Foo', category=category)
+    Criteria.objects.create(label='Baz', category=category)
+
+    app = login(app)
+    resp = app.get('/manage/pricing/criteria/category/%s/export/' % category.id)
+    assert resp.headers['content-type'] == 'application/json'
+    assert (
+        resp.headers['content-disposition']
+        == 'attachment; filename="export_pricing_category_foo-bar_20210708.json"'
+    )
+    category_export = resp.text
+
+    # existing category
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    resp = resp.follow()
+    assert (
+        'No pricing criteria category created. A pricing criteria category has been updated.' not in resp.text
+    )
+    assert CriteriaCategory.objects.count() == 1
+    assert Criteria.objects.count() == 2
+
+    # new category
+    CriteriaCategory.objects.all().delete()
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/criterias/')
+    resp = resp.follow()
+    assert (
+        'A pricing criteria category has been created. No pricing criteria category updated.' not in resp.text
+    )
+    assert CriteriaCategory.objects.count() == 1
+    assert Criteria.objects.count() == 2
+
+    # multiple categories
+    categories = json.loads(category_export)
+    categories['pricing_categories'].append(copy.copy(categories['pricing_categories'][0]))
+    categories['pricing_categories'].append(copy.copy(categories['pricing_categories'][0]))
+    categories['pricing_categories'][1]['label'] = 'Foo bar 2'
+    categories['pricing_categories'][1]['slug'] = 'foo-bar-2'
+    categories['pricing_categories'][2]['label'] = 'Foo bar 3'
+    categories['pricing_categories'][2]['slug'] = 'foo-bar-3'
+
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload(
+        'export.json', json.dumps(categories).encode('utf-8'), 'application/json'
+    )
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/')
+    resp = resp.follow()
+    assert (
+        '2 pricing criteria categories have been created. A pricing criteria category has been updated.'
+        in resp.text
+    )
+    assert CriteriaCategory.objects.count() == 3
+    assert Criteria.objects.count() == 6
+
+    CriteriaCategory.objects.all().delete()
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload(
+        'export.json', json.dumps(categories).encode('utf-8'), 'application/json'
+    )
+    resp = resp.form.submit().follow()
+    assert (
+        '3 pricing criteria categories have been created. No pricing criteria category updated.' in resp.text
+    )
+    assert CriteriaCategory.objects.count() == 3
+    assert Criteria.objects.count() == 6
+
+
+@pytest.mark.freeze_time('2021-07-08')
+def test_import_agenda(app, admin_user):
+    agenda = Agenda.objects.create(label='Foo Bar')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/agenda/%s/' % agenda.pk)
+    resp = resp.click('Export')
+    assert resp.headers['content-type'] == 'application/json'
+    assert (
+        resp.headers['content-disposition']
+        == 'attachment; filename="export_pricing_agenda_foo-bar_20210708.json"'
+    )
+    agenda_export = resp.text
+
+    # existing agenda
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', agenda_export.encode('utf-8'), 'application/json')
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/agenda/%s/' % agenda.pk)
+    resp = resp.follow()
+    assert 'An agenda has been updated.' not in resp.text
+    assert Agenda.objects.count() == 1
+
+    # unknown agenda
+    Agenda.objects.all().delete()
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', agenda_export.encode('utf-8'), 'application/json')
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['config_json'] == ['Missing "foo-bar" agenda']
+
+    # multiple pricing
+    Agenda.objects.create(label='Foo Bar')
+    Agenda.objects.create(label='Foo Bar 2')
+    agendas = json.loads(agenda_export)
+    agendas['agendas'].append(copy.copy(agendas['agendas'][0]))
+    agendas['agendas'][1]['slug'] = 'foo-bar-2'
+
+    resp = app.get('/manage/pricing/', status=200)
+    resp = resp.click('Import')
+    resp.form['config_json'] = Upload('export.json', json.dumps(agendas).encode('utf-8'), 'application/json')
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/')
+    resp = resp.follow()
+    assert '2 agendas have been updated.' in resp.text
+    assert Agenda.objects.count() == 2
diff --git a/tests/pricing/manager/test_pricing.py b/tests/pricing/manager/test_pricing.py
new file mode 100644
index 0000000..e1f3b13
--- /dev/null
+++ b/tests/pricing/manager/test_pricing.py
@@ -0,0 +1,324 @@
+import pytest
+
+from lingo.pricing.models import Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
+from tests.utils import login
+
+pytestmark = pytest.mark.django_db
+
+
+def test_add_pricing(app, admin_user):
+    app = login(app)
+    resp = app.get('/manage/')
+    resp = resp.click('Pricing')
+    resp = resp.click('New pricing model')
+    resp.form['label'] = 'Pricing model for lunch'
+    resp = resp.form.submit()
+    pricing = Pricing.objects.latest('pk')
+    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
+    assert pricing.label == 'Pricing model for lunch'
+    assert pricing.slug == 'pricing-model-for-lunch'
+
+
+def test_detail_pricing(app, admin_user):
+    pricing = Pricing.objects.create(label='Model')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/')
+    resp = resp.click(href='/manage/pricing/%s/' % pricing.pk)
+    assert '/manage/pricing/%s/edit/' % pricing.pk in resp
+    assert '/manage/pricing/%s/delete/' % pricing.pk in resp
+
+
+def test_edit_pricing(app, admin_user):
+    pricing = Pricing.objects.create(label='Model 1')
+    pricing2 = Pricing.objects.create(label='Model 2')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.click(href='/manage/pricing/%s/edit/' % pricing.pk)
+    resp.form['label'] = 'Model Foo'
+    resp.form['slug'] = pricing2.slug
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['slug'] == ['Pricing with this Identifier already exists.']
+
+    resp.form['slug'] = 'foo-bar'
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
+    pricing.refresh_from_db()
+    assert pricing.label == 'Model Foo'
+    assert pricing.slug == 'foo-bar'
+
+
+def test_delete_pricing(app, admin_user):
+    pricing = Pricing.objects.create(label='Model')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.click(href='/manage/pricing/%s/delete/' % pricing.pk)
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/')
+    assert Pricing.objects.exists() is False
+
+
+def test_duplicate_pricing(app, admin_user):
+    pricing = Pricing.objects.create(label='Model')
+    assert Pricing.objects.count() == 1
+
+    app = login(app)
+    resp = app.get('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.click(href='/manage/pricing/%s/duplicate/' % pricing.pk)
+    resp = resp.form.submit()
+    assert Pricing.objects.count() == 2
+
+    new_pricing = Pricing.objects.latest('pk')
+    assert resp.location == '/manage/pricing/%s/' % new_pricing.pk
+    assert new_pricing.pk != pricing.pk
+
+    resp = resp.follow()
+    assert 'copy-of-model' in resp.text
+
+    resp = resp.click(href='/manage/pricing/%s/duplicate/' % new_pricing.pk)
+    resp.form['label'] = 'hop'
+    resp = resp.form.submit().follow()
+    assert 'hop' in resp.text
+
+
+def test_pricing_edit_extra_variables(app, admin_user):
+    pricing = Pricing.objects.create(label='Model')
+    assert pricing.extra_variables == {}
+
+    app = login(app)
+    resp = app.get('/manage/pricing/%s/' % pricing.id)
+    assert '<label>Extra variables:</label>' not in resp.text
+    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
+    resp.form['form-0-key'] = 'foo'
+    resp.form['form-0-value'] = 'bar'
+    resp = resp.form.submit().follow()
+    pricing.refresh_from_db()
+    assert pricing.extra_variables == {'foo': 'bar'}
+    assert '<label>Extra variables:</label>' in resp.text
+    assert '<i>foo</i>' in resp
+
+    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
+    assert resp.form['form-TOTAL_FORMS'].value == '2'
+    assert resp.form['form-0-key'].value == 'foo'
+    assert resp.form['form-0-value'].value == 'bar'
+    assert resp.form['form-1-key'].value == ''
+    assert resp.form['form-1-value'].value == ''
+    resp.form['form-0-value'] = 'bar-bis'
+    resp.form['form-1-key'] = 'blah'
+    resp.form['form-1-value'] = 'baz'
+    resp = resp.form.submit().follow()
+    pricing.refresh_from_db()
+    assert pricing.extra_variables == {
+        'foo': 'bar-bis',
+        'blah': 'baz',
+    }
+    assert '<i>blah</i>, <i>foo</i>' in resp
+
+    resp = resp.click(href='/manage/pricing/%s/variable/' % pricing.id)
+    assert resp.form['form-TOTAL_FORMS'].value == '3'
+    assert resp.form['form-0-key'].value == 'blah'
+    assert resp.form['form-0-value'].value == 'baz'
+    assert resp.form['form-1-key'].value == 'foo'
+    assert resp.form['form-1-value'].value == 'bar-bis'
+    assert resp.form['form-2-key'].value == ''
+    assert resp.form['form-2-value'].value == ''
+    resp.form['form-1-key'] = 'foo'
+    resp.form['form-1-value'] = 'bar'
+    resp.form['form-0-key'] = ''
+    resp = resp.form.submit().follow()
+    pricing.refresh_from_db()
+    assert pricing.extra_variables == {
+        'foo': 'bar',
+    }
+    assert '<i>foo</i>' in resp
+
+
+def test_pricing_add_category(app, admin_user):
+    pricing = Pricing.objects.create(label='Model')
+    category1 = CriteriaCategory.objects.create(label='Cat 1')
+    category2 = CriteriaCategory.objects.create(label='Cat 2')
+    category3 = CriteriaCategory.objects.create(label='Cat 3')
+    category4 = CriteriaCategory.objects.create(label='Cat 4')
+
+    app = login(app)
+    resp = app.get('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.click(href='/manage/pricing/%s/category/add/' % pricing.pk)
+    assert list(resp.context['form'].fields['category'].queryset) == [
+        category1,
+        category2,
+        category3,
+        category4,
+    ]
+    resp.form['category'] = category1.pk
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.follow()
+    assert '/manage/pricing/%s/category/add/' % pricing.pk in resp
+    assert list(
+        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
+    ) == [category1.pk]
+    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
+        1
+    ]
+
+    resp = app.get('/manage/pricing/%s/category/add/' % pricing.pk)
+    assert list(resp.context['form'].fields['category'].queryset) == [category2, category3, category4]
+    resp.form['category'] = category4.pk
+    resp = resp.form.submit().follow()
+    assert list(
+        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
+    ) == [category1.pk, category4.pk]
+    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
+        1,
+        2,
+    ]
+
+    resp = app.get('/manage/pricing/%s/category/add/' % pricing.pk)
+    assert list(resp.context['form'].fields['category'].queryset) == [category2, category3]
+    resp.form['category'] = category2.pk
+    resp = resp.form.submit().follow()
+    assert '/manage/pricing/%s/category/add/' % pricing.pk not in resp
+    assert list(
+        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
+    ) == [category1.pk, category4.pk, category2.pk]
+    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
+        1,
+        2,
+        3,
+    ]
+
+    app.get('/manage/pricing/%s/category/add/' % pricing.pk, status=404)
+
+
+def test_pricing_edit_category(app, admin_user):
+    category1 = CriteriaCategory.objects.create(label='Cat 1')
+    criteria1 = Criteria.objects.create(label='Crit 1', category=category1)
+    criteria2 = Criteria.objects.create(label='Crit 2', category=category1)
+    criteria3 = Criteria.objects.create(label='Crit 3', category=category1)
+    criteria4 = Criteria.objects.create(label='Crit 4', category=category1)
+    category2 = CriteriaCategory.objects.create(label='cat 2')
+    criteria5 = Criteria.objects.create(label='Crit 5', category=category2)
+    pricing = Pricing.objects.create(label='Model')
+    pricing.categories.add(category1, through_defaults={'order': 1})
+    pricing.categories.add(category2, through_defaults={'order': 2})
+
+    app = login(app)
+    resp = app.get('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.click(href='/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category1.pk))
+    assert list(resp.context['form'].fields['criterias'].queryset) == [
+        criteria1,
+        criteria2,
+        criteria3,
+        criteria4,
+    ]
+    assert list(resp.context['form'].initial['criterias']) == []
+    resp.form['criterias'] = [criteria1.pk, criteria3.pk]
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.follow()
+    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria3]
+
+    resp = app.get('/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category1.pk))
+    assert list(resp.context['form'].initial['criterias']) == [criteria1, criteria3]
+    resp.form['criterias'] = [criteria1.pk, criteria4.pk]
+    resp = resp.form.submit().follow()
+    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria4]
+
+    resp = app.get('/manage/pricing/%s/category/%s/edit/' % (pricing.pk, category2.pk))
+    assert list(resp.context['form'].fields['criterias'].queryset) == [criteria5]
+    assert list(resp.context['form'].initial['criterias']) == []
+    resp.form['criterias'] = [criteria5.pk]
+    resp = resp.form.submit().follow()
+    assert list(pricing.criterias.order_by('pk')) == [criteria1, criteria4, criteria5]
+
+
+def test_pricing_delete_category(app, admin_user):
+    category1 = CriteriaCategory.objects.create(label='Cat 1')
+    criteria1 = Criteria.objects.create(label='Crit 1', category=category1)
+    category2 = CriteriaCategory.objects.create(label='Cat 2')
+    criteria2 = Criteria.objects.create(label='Crit 2', category=category2)
+    pricing = Pricing.objects.create(label='Model')
+    pricing.categories.add(category1, through_defaults={'order': 1})
+    pricing.categories.add(category2, through_defaults={'order': 2})
+    pricing.criterias.add(criteria1, criteria2)
+
+    app = login(app)
+    resp = app.get('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.click(href='/manage/pricing/%s/category/%s/delete/' % (pricing.pk, category1.pk))
+    resp = resp.form.submit()
+    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
+    resp = resp.follow()
+    assert list(pricing.categories.all()) == [category2]
+    assert list(pricing.criterias.all()) == [criteria2]
+
+    # not linked
+    app.get('/manage/pricing/%s/category/%s/delete/' % (pricing.pk, category1.pk), status=404)
+    # unknown
+    app.get('/manage/pricing/%s/category/%s/delete/' % (pricing.pk, 0), status=404)
+
+
+def test_pricing_reorder_categories(app, admin_user):
+    category1 = CriteriaCategory.objects.create(label='Cat 1')
+    category2 = CriteriaCategory.objects.create(label='Cat 2')
+    category3 = CriteriaCategory.objects.create(label='Cat 3')
+    category4 = CriteriaCategory.objects.create(label='Cat 4')
+    pricing = Pricing.objects.create(label='Model')
+    pricing.categories.add(category1, through_defaults={'order': 1})
+    pricing.categories.add(category2, through_defaults={'order': 2})
+    pricing.categories.add(category3, through_defaults={'order': 3})
+    assert list(
+        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
+    ) == [category1.pk, category2.pk, category3.pk]
+    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
+        1,
+        2,
+        3,
+    ]
+
+    app = login(app)
+    # missing get params
+    app.get('/manage/pricing/%s/order/' % (pricing.pk), status=400)
+
+    # bad new-order param
+    bad_params = [
+        # missing category3 in order
+        ','.join(str(x) for x in [category1.pk, category2.pk]),
+        # category1 mentionned twice
+        ','.join(str(x) for x in [category1.pk, category2.pk, category3.pk, category1.pk]),
+        # category4 not in pricing categories
+        ','.join(str(x) for x in [category1.pk, category2.pk, category3.pk, category4.pk]),
+        # not an id
+        'foo,1,2,3',
+        ' 1 ,2,3',
+    ]
+    for bad_param in bad_params:
+        app.get(
+            '/manage/pricing/%s/order/' % (pricing.pk),
+            params={'new-order': bad_param},
+            status=400,
+        )
+    # not changed
+    assert list(
+        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
+    ) == [category1.pk, category2.pk, category3.pk]
+    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
+        1,
+        2,
+        3,
+    ]
+
+    # change order
+    app.get(
+        '/manage/pricing/%s/order/' % (pricing.pk),
+        params={'new-order': ','.join(str(x) for x in [category3.pk, category1.pk, category2.pk])},
+    )
+    assert list(
+        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
+    ) == [category3.pk, category1.pk, category2.pk]
+    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
+        1,
+        2,
+        3,
+    ]
-- 
2.35.1

