From 4008d53267dba21ded57dbbb86ed87a29b4b47cc Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 25 Nov 2024 16:57:09 +0100 Subject: [PATCH 01/10] :white_check_mark: [#4771] Add migration tests for the desired behaviour before and after migrating --- .../0106_convert_price_logic_rules.py | 12 ++ src/openforms/forms/tests/test_migrations.py | 152 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 src/openforms/forms/migrations/0106_convert_price_logic_rules.py create mode 100644 src/openforms/forms/tests/test_migrations.py diff --git a/src/openforms/forms/migrations/0106_convert_price_logic_rules.py b/src/openforms/forms/migrations/0106_convert_price_logic_rules.py new file mode 100644 index 0000000000..4ca4905bf1 --- /dev/null +++ b/src/openforms/forms/migrations/0106_convert_price_logic_rules.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.16 on 2024-11-25 15:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("forms", "0105_alter_form_all_submissions_removal_limit_and_more"), + ] + + operations = [] diff --git a/src/openforms/forms/tests/test_migrations.py b/src/openforms/forms/tests/test_migrations.py new file mode 100644 index 0000000000..672c4d2696 --- /dev/null +++ b/src/openforms/forms/tests/test_migrations.py @@ -0,0 +1,152 @@ +from decimal import Decimal + +from django.db.migrations.state import StateApps + +from openforms.submissions.form_logic import check_submission_logic +from openforms.submissions.models import ( + Submission, + SubmissionStep, + SubmissionValueVariable, +) +from openforms.submissions.pricing import get_submission_price +from openforms.utils.tests.test_migrations import TestMigrations +from openforms.variables.constants import FormVariableDataTypes, FormVariableSources + + +def _persist_user_defined_variables(submission): + # inspired by openforms.submissions.utils.persist_user_defined_variables + data = submission.data + check_submission_logic(submission, data) + state = submission.load_submission_value_variables_state() + variables = state.variables + + user_defined_vars_data = { + variable.key: variable.value + for variable in variables.values() + if variable.form_variable + and variable.form_variable.source == FormVariableSources.user_defined + } + + if user_defined_vars_data: + SubmissionValueVariable.objects.bulk_create_or_update_from_data( + user_defined_vars_data, submission + ) + + +class FormLogicMigrationTests(TestMigrations): + app = "forms" + migrate_from = "0105_alter_form_all_submissions_removal_limit_and_more" + migrate_to = "0106_convert_price_logic_rules" + + def setUpBeforeMigration(self, apps: StateApps): + # set up some variants that will each be hit for different submissions. After + # migrating to form variable pricing, the result must be the same. + Product = apps.get_model("products", "Product") + Form = apps.get_model("forms", "Form") + FormDefinition = apps.get_model("forms", "FormDefinition") + FormStep = apps.get_model("forms", "FormStep") + FormPriceLogic = apps.get_model("forms", "FormPriceLogic") + FormVariable = apps.get_model("forms", "FormVariable") + + product = Product.objects.create(name="Test product", price=Decimal("4.12")) + fd = FormDefinition.objects.create( + name="Pricing tests", configuration={"components": []} + ) + form = Form.objects.create(name="Pricing tests", product=product, slug="step-1") + form_step = FormStep.objects.create(form=form, form_definition=fd, order=1) + amount_variable = FormVariable.objects.create( + form=form, + name="Amount", + key="amount", + source=FormVariableSources.user_defined, + data_type=FormVariableDataTypes.float, + initial_value=1.0, + ) + FormPriceLogic.objects.create( + form=form, + json_logic_trigger={"==": [{"var": "amount"}, 3]}, + price=Decimal("11.99"), + ) + FormPriceLogic.objects.create( + form=form, + json_logic_trigger={"==": [{"var": "amount"}, 5]}, + price=Decimal("19.99"), + ) + + # we deliberately use the real models here to get access to the properties and + # custom methods so that we can call get_submission_price. This requires + # migrations to be properly orchestrated so that no schema migrations in the + # submissions app happen at the wrong time! It could be a possible future + # failure point. + + submission1 = Submission.objects.create(form_id=form.id) + SubmissionStep.objects.create(submission=submission1, form_step_id=form_step.id) + self.submission1_pk = submission1.pk + SubmissionValueVariable.objects.create( + submission=submission1, + form_variable_id=amount_variable.id, + key="amount", + value=1.0, + ) + price1 = get_submission_price(submission1) + assert price1 == Decimal("4.12") + + submission2 = Submission.objects.create(form_id=form.id) + SubmissionStep.objects.create(submission=submission2, form_step_id=form_step.id) + self.submission2_pk = submission2.pk + SubmissionValueVariable.objects.create( + submission=submission2, + form_variable_id=amount_variable.id, + key="amount", + value=3, + ) + price2 = get_submission_price(submission2) + assert price2 == Decimal("11.99") + + submission3 = Submission.objects.create(form_id=form.id) + SubmissionStep.objects.create(submission=submission3, form_step_id=form_step.id) + self.submission3_pk = submission3.pk + SubmissionValueVariable.objects.create( + submission=submission3, + form_variable_id=amount_variable.id, + key="amount", + value=5, + ) + price3 = get_submission_price(submission3) + assert price3 == Decimal("19.99") + + def test_prices_still_the_same_after_migration(self): + with self.subTest("submission 1"): + submission1 = Submission.objects.get(pk=self.submission1_pk) + _persist_user_defined_variables(submission1) + + price1 = get_submission_price(submission1) + + self.assertEqual(price1, Decimal("4.12")) + + with self.subTest("submission 2"): + submission2 = Submission.objects.get(pk=self.submission2_pk) + _persist_user_defined_variables(submission2) + + price2 = get_submission_price(submission2) + + self.assertEqual(price2, Decimal("11.99")) + + with self.subTest("submission 3"): + submission3 = Submission.objects.get(pk=self.submission3_pk) + _persist_user_defined_variables(submission3) + + price3 = get_submission_price(submission3) + + self.assertEqual(price3, Decimal("19.99")) + + def test_price_variable_created(self): + Form = self.apps.get_model("forms", "Form") + form = Form.objects.get() + + variables = {variable.key: variable for variable in form.formvariable_set.all()} + + self.assertIn("totalPrice", variables) + variable = variables["totalPrice"] + self.assertEqual(variable.data_type, FormVariableDataTypes.float) + self.assertEqual(variable.source, FormVariableSources.user_defined) From b0f704f74f28482529dca4552b7e6a1de5a9b42a Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 25 Nov 2024 18:10:31 +0100 Subject: [PATCH 02/10] :card_file_box: [#4771] Add data migration to convert price logic rules to normal logic Converted the current behaviour of price logic rules into assignments of the price logic variable value, including the fallback behaviour to the product price if no logic rule matches. This last bit is slightly nuanced, since changing the price on the product no longer results in the fallback case being automatically updated along, and that requires a note in the changelog (!). At first, I wanted to trigger the logic rules only when the last step was reached, using trigger_from_step, but I'm unsure that it behaves correctly if the last step is somehow made not applicable with other logic rules, so instead these rules are always evaluated. If it's safe, users can update the auto-migrated rules to optimize performance. I've also opted to not implement the reverse operation due to the involved complexity - we should definitely recommend to make database backups before upgrading to 3.0. We can be a bit bolder than usualy because it's part of a major version with other breaking changes anyway, and it should be fine (tm) for the majority of the cases. --- .../0106_convert_price_logic_rules.py | 85 ++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/openforms/forms/migrations/0106_convert_price_logic_rules.py b/src/openforms/forms/migrations/0106_convert_price_logic_rules.py index 4ca4905bf1..1cba09d14a 100644 --- a/src/openforms/forms/migrations/0106_convert_price_logic_rules.py +++ b/src/openforms/forms/migrations/0106_convert_price_logic_rules.py @@ -1,6 +1,85 @@ # Generated by Django 4.2.16 on 2024-11-25 15:32 +from decimal import Decimal from django.db import migrations +from django.db.migrations.state import StateApps + +from openforms.forms.constants import LogicActionTypes +from openforms.variables.constants import FormVariableDataTypes, FormVariableSources + +VARIABLE_NAME = "Total price" +VARIABLE_KEY = "totalPrice" + + +def _assignment_action(key: str, value: Decimal): + return { + "variable": key, + "action": { + "type": LogicActionTypes.variable, + "value": str(value), + }, + } + + +def convert_price_logic_rules_to_price_variable(apps: StateApps, _): + """ + For each form that has price logic rules, create a variable to hold the price and + add normal logic rules. + """ + Form = apps.get_model("forms", "Form") + forms_with_pricelogic = ( + Form.objects.filter(formpricelogic__isnull=False) + .exclude(product__isnull=True) + .distinct() + ) + + for form in forms_with_pricelogic.iterator(): + product = form.product + rules = form.formpricelogic_set.all() + + # create a variable to hold the result. + # TODO: handle possible key collissions + price_variable = form.formvariable_set.create( + form_definition=None, + name=VARIABLE_NAME, + key=VARIABLE_KEY, + source=FormVariableSources.user_defined, + data_type=FormVariableDataTypes.float, + ) + form.price_variable_key = price_variable.key + form.save() + + max_order = ( + last_rule.order + if (last_rule := form.formlogic_set.order_by("order").last()) + else 0 + ) + + # set up regular logic rules for each price logic rule + for rule in rules: + max_order += 1 + form.formlogic_set.create( + description="Converted price logic rule", + order=max_order, + is_advanced=True, + json_logic_trigger=rule.json_logic_trigger, + actions=[_assignment_action(form.price_variable_key, rule.price)], + ) + + # create one fallback rule in case none of the triggers hit + composite_negated_trigger = { + "!": {"or": [rule.json_logic_trigger for rule in rules]} + } + max_order += 1 + form.formlogic_set.create( + description="Converted price logic rule", + order=max_order, + is_advanced=True, + json_logic_trigger=composite_negated_trigger, + actions=[_assignment_action(form.price_variable_key, product.price)], + ) + + rules.delete() class Migration(migrations.Migration): @@ -9,4 +88,8 @@ class Migration(migrations.Migration): ("forms", "0105_alter_form_all_submissions_removal_limit_and_more"), ] - operations = [] + operations = [ + migrations.RunPython( + convert_price_logic_rules_to_price_variable, migrations.RunPython.noop + ), + ] From f81883863d1dbfd98f947694399fd988ff414123 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 25 Nov 2024 18:41:15 +0100 Subject: [PATCH 03/10] :card_file_box: [#4117] Handle edge case where a variable with the same key already exists --- .../0106_convert_price_logic_rules.py | 18 +++++- src/openforms/forms/tests/test_migrations.py | 58 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/openforms/forms/migrations/0106_convert_price_logic_rules.py b/src/openforms/forms/migrations/0106_convert_price_logic_rules.py index 1cba09d14a..885c92a45e 100644 --- a/src/openforms/forms/migrations/0106_convert_price_logic_rules.py +++ b/src/openforms/forms/migrations/0106_convert_price_logic_rules.py @@ -38,11 +38,23 @@ def convert_price_logic_rules_to_price_variable(apps: StateApps, _): rules = form.formpricelogic_set.all() # create a variable to hold the result. - # TODO: handle possible key collissions + variable_keys = set(form.formvariable_set.values_list("key", flat=True)) + variable_key = VARIABLE_KEY + variable_name = VARIABLE_NAME + counter = 0 + while variable_key in variable_keys: + counter += 1 + variable_key = f"{variable_key}{counter}" + variable_name = f"{variable_name}{counter}" + if counter > 100: + raise RuntimeError( + "Could not generate a unique key without looping too long" + ) + price_variable = form.formvariable_set.create( form_definition=None, - name=VARIABLE_NAME, - key=VARIABLE_KEY, + name=variable_name, + key=variable_key, source=FormVariableSources.user_defined, data_type=FormVariableDataTypes.float, ) diff --git a/src/openforms/forms/tests/test_migrations.py b/src/openforms/forms/tests/test_migrations.py index 672c4d2696..95676f0d8a 100644 --- a/src/openforms/forms/tests/test_migrations.py +++ b/src/openforms/forms/tests/test_migrations.py @@ -150,3 +150,61 @@ def test_price_variable_created(self): variable = variables["totalPrice"] self.assertEqual(variable.data_type, FormVariableDataTypes.float) self.assertEqual(variable.source, FormVariableSources.user_defined) + + +class DuplicatePriceVariableMigrationTests(TestMigrations): + app = "forms" + migrate_from = "0105_alter_form_all_submissions_removal_limit_and_more" + migrate_to = "0106_convert_price_logic_rules" + + def setUpBeforeMigration(self, apps: StateApps): + # set up some variants that will each be hit for different submissions. After + # migrating to form variable pricing, the result must be the same. + Product = apps.get_model("products", "Product") + Form = apps.get_model("forms", "Form") + FormDefinition = apps.get_model("forms", "FormDefinition") + FormStep = apps.get_model("forms", "FormStep") + FormPriceLogic = apps.get_model("forms", "FormPriceLogic") + FormVariable = apps.get_model("forms", "FormVariable") + + product = Product.objects.create(name="Test product", price=Decimal("4.12")) + fd = FormDefinition.objects.create( + name="Pricing tests", configuration={"components": []} + ) + form = Form.objects.create(name="Pricing tests", product=product, slug="step-1") + FormStep.objects.create(form=form, form_definition=fd, order=1) + FormPriceLogic.objects.create( + form=form, + json_logic_trigger={"==": [{"var": "amount"}, 3]}, + price=Decimal("11.99"), + ) + FormVariable.objects.create( + form=form, + name="Amount", + key="amount", + source=FormVariableSources.user_defined, + data_type=FormVariableDataTypes.float, + initial_value=1.0, + ) + # causes conflicts + FormVariable.objects.create( + form=form, + name="Total price", + key="totalPrice", + source=FormVariableSources.user_defined, + data_type=FormVariableDataTypes.float, + initial_value=10.0, + ) + + def test_price_variable_created(self): + Form = self.apps.get_model("forms", "Form") + form = Form.objects.get() + + variables = {variable.key: variable for variable in form.formvariable_set.all()} + + self.assertIn("totalPrice", variables) # pre-existing + self.assertIn("totalPrice1", variables) # newly created + variable = variables["totalPrice1"] + self.assertEqual(variable.data_type, FormVariableDataTypes.float) + self.assertEqual(variable.source, FormVariableSources.user_defined) + self.assertEqual(form.price_variable_key, "totalPrice1") From 5e2c64e14206534694410ce50b479b9aed820ac6 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 25 Nov 2024 18:59:20 +0100 Subject: [PATCH 04/10] :fire: [#4771] Delete UI code for price logic rules Price logic rules are replaced with normal logic rules that assign the calculated price to a variable, and instead you should point to the form variable holding the calculated value. --- .../admin/form_design/PriceLogic.js | 130 +----------------- .../admin/form_design/data/complete-form.js | 40 +----- .../admin/form_design/data/logic.js | 4 +- .../admin/form_design/data/read-form.js | 10 +- .../admin/form_design/form-creation-form.js | 64 +-------- 5 files changed, 16 insertions(+), 232 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/PriceLogic.js b/src/openforms/js/components/admin/form_design/PriceLogic.js index 650c0ffa09..c4c9d1ece2 100644 --- a/src/openforms/js/components/admin/form_design/PriceLogic.js +++ b/src/openforms/js/components/admin/form_design/PriceLogic.js @@ -1,30 +1,14 @@ import PropTypes from 'prop-types'; -import React, {useContext, useState} from 'react'; +import React, {useState} from 'react'; import {FormattedMessage, defineMessage, useIntl} from 'react-intl'; -import ButtonContainer from 'components/admin/forms/ButtonContainer'; import Field from 'components/admin/forms/Field'; import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; -import {NumberInput} from 'components/admin/forms/Inputs'; import Select from 'components/admin/forms/Select'; -import {ValidationErrorContext} from 'components/admin/forms/ValidationErrors'; import VariableSelection from 'components/admin/forms/VariableSelection'; -import {DeleteIcon} from 'components/admin/icons'; import {getTranslatedChoices} from 'utils/i18n'; -import DSLEditorNode from './logic/DSLEditorNode'; -import Trigger from './logic/Trigger'; -import {parseValidationErrors} from './utils'; - -export const EMPTY_PRICE_RULE = { - uuid: '', - _generatedId: '', // consumers should generate this, as it's used for the React key prop if no uuid exists - form: '', - jsonLogicTrigger: {}, - price: '', -}; - const PRICING_MODES = [ [ 'static', @@ -40,13 +24,6 @@ const PRICING_MODES = [ defaultMessage: 'Use a variable for the price', }), ], - [ - 'dynamic', - defineMessage({ - description: 'dynamic pricing mode label', - defaultMessage: 'Use logic rules to determine the price', - }), - ], ]; const PricingMode = ({mode = 'static', onChange}) => { @@ -61,57 +38,21 @@ PricingMode.propTypes = { onChange: PropTypes.func.isRequired, }; -export const PriceLogic = ({variableKey, rules = [], onChange, onDelete, onAdd, onFieldChange}) => { - const initialPricingMode = - variableKey !== '' ? 'variable' : rules.length > 0 ? 'dynamic' : 'static'; +export const PriceLogic = ({variableKey, onFieldChange}) => { + const initialPricingMode = variableKey !== '' ? 'variable' : 'static'; const [pricingMode, setPricingMode] = useState(initialPricingMode); - const validationErrors = parseValidationErrors(useContext(ValidationErrorContext), 'priceRules'); - - // TODO: de-duplicate/validate duplicate rules (identical triggers?) - const onPricingModeChange = event => { const {value} = event.target; - const resetVariableKey = () => { - if (variableKey) { - onFieldChange({target: {name: 'form.priceVariableKey', value: ''}}); - } - }; - - const resetRules = () => { - if (rules.length > 0) { - // XXX: iterate in reverse so we delete all rules by removing the last one - // every time. - // State updates in event handlers are batched by React, so removing index 0, 1,... - // in success causes issues since the local `rules` does no langer match the - // parent component state - draft.priceRules.length !== rules.length. - // By reversing, we essentially pop the last element every time which - // works around this. - const maxIndex = rules.length - 1; - for (let offset = 0; offset < rules.length; offset++) { - onDelete(maxIndex - offset); - } - } - }; - switch (value) { case 'variable': { - resetRules(); - break; - } - case 'dynamic': { - // toggle from static to dynamic -> ensure at least one rule exists - if (rules.length === 0) { - onAdd(); - } - resetVariableKey(); break; } case 'static': { - // toggle from dynamic to static -> delete all the rules - resetRules(); - resetVariableKey(); + if (variableKey) { + onFieldChange({target: {name: 'form.priceVariableKey', value: ''}}); + } break; } } @@ -138,22 +79,6 @@ export const PriceLogic = ({variableKey, rules = [], onChange, onDelete, onAdd, - {rules.map((rule, index) => ( - - ))} - - {pricingMode === 'dynamic' && ( - - - - )} - {pricingMode === 'variable' && ( { - const intl = useIntl(); - const deleteConfirmMessage = intl.formatMessage({ - description: 'Price rule deletion confirm message', - defaultMessage: 'Are you sure you want to delete this rule?', - }); - return ( -
-
- -
- -
- - - -  €  - - - -
-
- ); -}; - -Rule.propTypes = { - jsonLogicTrigger: PropTypes.object, - price: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - onChange: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - errors: PropTypes.object, -}; - export default PriceLogic; diff --git a/src/openforms/js/components/admin/form_design/data/complete-form.js b/src/openforms/js/components/admin/form_design/data/complete-form.js index 39cc41f608..d1f2321be2 100644 --- a/src/openforms/js/components/admin/form_design/data/complete-form.js +++ b/src/openforms/js/components/admin/form_design/data/complete-form.js @@ -91,7 +91,6 @@ const handleAppointmentForm = draft => { draft.stepsToDelete = draft.formSteps.map(step => step.url).filter(Boolean); draft.formSteps = []; draft.logicRules = []; - draft.priceRules = []; draft.formVariables = []; }; @@ -221,7 +220,7 @@ const saveSteps = async (state, csrftoken) => { }; /** - * Save the logic rules and price rules, report back any validation errors. + * Save the logic rules, report back any validation errors. */ const saveLogic = async (state, csrftoken) => { const { @@ -239,23 +238,14 @@ const saveLogic = async (state, csrftoken) => { for (const action of rule.actions) { action.formStep = getStepReference(stepsByGeneratedId, action.formStep); } - - for (const rule of draft.priceRules) { - rule.form = formUrl; - } } }); // make the actual API call let errors = []; - let responseLogicRules, responsePriceRules; + let responseLogicRules; try { - responseLogicRules = await createOrUpdateLogicRules( - formUrl, - newState.logicRules, - csrftoken, - false - ); + responseLogicRules = await createOrUpdateLogicRules(formUrl, newState.logicRules, csrftoken); newState = produce(newState, draft => { draft.logicRules = responseLogicRules.data; @@ -274,28 +264,6 @@ const saveLogic = async (state, csrftoken) => { } } - try { - responsePriceRules = await createOrUpdateLogicRules( - formUrl, - newState.priceRules, - csrftoken, - true - ); - - newState = produce(newState, draft => { - draft.priceRules = responsePriceRules.data; - }); - } catch (e) { - if (e instanceof ValidationErrors) { - e.context = 'priceRules'; - // TODO: convert in list of errors for further processing? - errors = errors.concat([e]); - } else { - // re-throw any other type of error - throw e; - } - } - return [newState, errors]; }; @@ -435,7 +403,7 @@ const saveCompleteForm = async (state, csrftoken) => { // since the logic rules validate if the variables in the trigger exist. [newState, variableValidationErrors] = await saveVariables(newState, csrftoken); - // save (normal) logic and price logic rules + // save logic rules [newState, logicValidationErrors] = await saveLogic(newState, csrftoken); const validationErrors = [...logicValidationErrors, ...variableValidationErrors]; diff --git a/src/openforms/js/components/admin/form_design/data/logic.js b/src/openforms/js/components/admin/form_design/data/logic.js index 2b672d1a29..228bfbc8f8 100644 --- a/src/openforms/js/components/admin/form_design/data/logic.js +++ b/src/openforms/js/components/admin/form_design/data/logic.js @@ -1,7 +1,7 @@ import {put} from 'utils/fetch'; -const createOrUpdateLogicRules = async (formUrl, logicRules, csrftoken, isPriceRule = false) => { - const endpoint = isPriceRule ? `${formUrl}/price-logic-rules` : `${formUrl}/logic-rules`; +const createOrUpdateLogicRules = async (formUrl, logicRules, csrftoken) => { + const endpoint = `${formUrl}/logic-rules`; return await put(endpoint, csrftoken, logicRules, true); }; diff --git a/src/openforms/js/components/admin/form_design/data/read-form.js b/src/openforms/js/components/admin/form_design/data/read-form.js index 3a621b2434..de00d823ff 100644 --- a/src/openforms/js/components/admin/form_design/data/read-form.js +++ b/src/openforms/js/components/admin/form_design/data/read-form.js @@ -17,7 +17,6 @@ const loadForm = async formUuid => { get(`${FORM_ENDPOINT}/${formUuid}/steps`), get(`${FORM_ENDPOINT}/${formUuid}/variables?source=${VARIABLE_SOURCES.userDefined}`), get(`${FORM_ENDPOINT}/${formUuid}/logic-rules`), - get(`${FORM_ENDPOINT}/${formUuid}/price-logic-rules`), ]; const responses = await Promise.all(requests); @@ -25,13 +24,7 @@ const loadForm = async formUuid => { throw new Error('An error occurred while loading the form data.'); } - const [ - formResponse, - formStepsResponse, - formVariablesResponse, - logicRulesResponse, - priceRulesResponse, - ] = responses; + const [formResponse, formStepsResponse, formVariablesResponse, logicRulesResponse] = responses; const form = formResponse.data; @@ -41,7 +34,6 @@ const loadForm = async formUuid => { steps: formStepsResponse.data, variables: formVariablesResponse.data, logicRules: logicRulesResponse.data, - priceRules: priceRulesResponse.data, }; }; diff --git a/src/openforms/js/components/admin/form_design/form-creation-form.js b/src/openforms/js/components/admin/form_design/form-creation-form.js index acb9ea330f..3fd8752430 100644 --- a/src/openforms/js/components/admin/form_design/form-creation-form.js +++ b/src/openforms/js/components/admin/form_design/form-creation-form.js @@ -33,7 +33,7 @@ import FormSteps from './FormSteps'; import FormSubmit from './FormSubmit'; import {DEFAULT_LANGUAGE} from './LanguageTabs'; import PaymentFields from './PaymentFields'; -import {EMPTY_PRICE_RULE, PriceLogic} from './PriceLogic'; +import PriceLogic from './PriceLogic'; import ProductFields from './ProductFields'; import RegistrationFields from './RegistrationFields'; import Tab from './Tab'; @@ -138,7 +138,6 @@ const initialFormState = { stepsToDelete: [], submitting: false, logicRules: [], - priceRules: [], formVariables: [], staticVariables: [], // backend error handling @@ -179,7 +178,6 @@ const FORM_FIELDS_TO_TAB_NAMES = { paymentBackendOptions: 'product-payment', submissionsRemovalOptions: 'submission-removal-options', logicRules: 'logic-rules', - priceRules: 'product-payment', variables: 'variables', appointmentOptions: 'form', brpPersonenRequestOptions: 'advanced-configuration', @@ -203,7 +201,7 @@ function reducer(draft, action) { */ case 'BACKEND_DATA_LOADED': { const {supportingData, formData} = action.payload; - const {form, selectedAuthPlugins, steps, variables, logicRules, priceRules} = formData; + const {form, selectedAuthPlugins, steps, variables, logicRules} = formData; for (const [stateVar, data] of Object.entries(supportingData)) { draft[stateVar] = data; @@ -219,7 +217,6 @@ function reducer(draft, action) { // if there's a description set already, it may not be mutated _mayGenerateDescription: !rule.description, })); - if (priceRules) draft.priceRules = priceRules; if (!draft.form.confirmationEmailTemplate) { draft.form.confirmationEmailTemplate = {subject: '', content: '', translations: {}}; @@ -837,46 +834,6 @@ function reducer(draft, action) { break; } - /** - * Price rules actions - */ - case 'ADD_PRICE_RULE': { - const { - form: {url}, - } = draft; - draft.priceRules.push({ - ...EMPTY_PRICE_RULE, - form: url, - _generatedId: getUniqueRandomString(), - }); - break; - } - case 'CHANGED_PRICE_RULE': { - const {index, name, value} = action.payload; - draft.priceRules[index][name] = value; - - const [validationErrors, tabsWithErrors] = updateWarningsValidationError( - draft.validationErrors, - draft.tabsWithErrors, - 'priceRules', - index, - name, - FORM_FIELDS_TO_TAB_NAMES['priceRules'] - ); - draft.validationErrors = validationErrors; - draft.tabsWithErrors = tabsWithErrors; - break; - } - case 'DELETED_PRICE_RULE': { - const {index} = action.payload; - - // delete object from state - const updatedRules = [...draft.priceRules]; - updatedRules.splice(index, 1); - draft.priceRules = updatedRules; - break; - } - /** * Submit & validation error handling */ @@ -1171,14 +1128,6 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl, outgoingRequestsUr }); }; - const onPriceRuleChange = (index, event) => { - const {name, value} = event.target; - dispatch({ - type: 'CHANGED_PRICE_RULE', - payload: {name, value, index}, - }); - }; - const onSubmit = async event => { const {name: submitAction} = event.target; const isCreate = state.newForm; @@ -1495,14 +1444,7 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl, outgoingRequestsUr backendOptions={state.form.paymentBackendOptions} onChange={onFieldChange} /> - dispatch({type: 'DELETED_PRICE_RULE', payload: {index: index}})} - onAdd={() => dispatch({type: 'ADD_PRICE_RULE'})} - onFieldChange={onFieldChange} - /> + )} From 9233057d0f8c143df6bcf0710449fb9326a383ff Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 25 Nov 2024 19:02:44 +0100 Subject: [PATCH 05/10] :fire: [#4771] Delete API endpoints to manage price logic rules --- src/openapi.yaml | 253 ------------------ .../forms/api/serializers/__init__.py | 2 - .../api/serializers/logic/form_logic_price.py | 22 -- src/openforms/forms/api/viewsets.py | 76 ------ .../tests/test_api_form_price_logic_bulk.py | 192 ------------- 5 files changed, 545 deletions(-) delete mode 100644 src/openforms/forms/api/serializers/logic/form_logic_price.py delete mode 100644 src/openforms/forms/tests/test_api_form_price_logic_bulk.py diff --git a/src/openapi.yaml b/src/openapi.yaml index 7eb8b7adf9..2a1e223c33 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -2512,229 +2512,6 @@ paths: $ref: '#/components/headers/X-Is-Form-Designer' Content-Language: $ref: '#/components/headers/Content-Language' - /api/v2/forms/{uuid_or_slug}/price-logic-rules: - get: - operationId: forms_price_logic_rules_list - description: List all price logic rules defined for a form. - summary: List price logic rules - parameters: - - in: path - name: uuid_or_slug - schema: - type: integer - description: A unique integer value identifying this form. - required: true - tags: - - logic-rules - security: - - tokenAuth: [] - - cookieAuth: [] - responses: - '200': - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/FormPriceLogic' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '401': - content: - application/json: - schema: - $ref: '#/components/schemas/Exception' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/Exception' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '404': - content: - application/json: - schema: - $ref: '#/components/schemas/Exception' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '405': - content: - application/json: - schema: - $ref: '#/components/schemas/Exception' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - put: - operationId: forms_price_logic_rules_update - description: By sending a list of FormPriceLogic to this endpoint, all the FormPriceLogic - related to the form will be replaced with the data sent to the endpoint. - summary: Bulk configure price logic rules - parameters: - - in: header - name: X-CSP-Nonce - schema: - type: string - description: The value of the CSP nonce generated by the page embedding the - SDK. If provided, fields containing rich text from WYSIWYG editors will - be post-processed to allow inline styles with the provided nonce. If the - embedding page emits a `style-src` policy containing `unsafe-inline`, then - you can omit this header without losing functionality. We recommend favouring - the nonce mechanism though. - - in: path - name: uuid_or_slug - schema: - type: integer - description: A unique integer value identifying this form. - required: true - tags: - - logic-rules - requestBody: - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/FormPriceLogic' - required: true - security: - - tokenAuth: [] - - cookieAuth: [] - responses: - '200': - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/FormPriceLogic' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '401': - content: - application/json: - schema: - $ref: '#/components/schemas/Exception' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/Exception' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '404': - content: - application/json: - schema: - $ref: '#/components/schemas/Exception' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - '405': - content: - application/json: - schema: - $ref: '#/components/schemas/Exception' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' /api/v2/forms/{uuid_or_slug}/variables: get: operationId: forms_variables_list @@ -8471,36 +8248,6 @@ components: title: Explanation template [nl] description: Content that will be shown on the start page of the form, below the title and above the log in text. - FormPriceLogic: - type: object - properties: - uuid: - type: string - format: uuid - readOnly: true - url: - type: string - format: uri - readOnly: true - form: - type: string - format: uri - description: Form to which the pricing JSON logic applies. - jsonLogicTrigger: - title: JSON logic - description: The trigger expression to determine if the actions should execute - or not. Note that this must be a valid JsonLogic expression, and the first - operand must be a reference to a variable in the form. - price: - type: string - format: decimal - pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ - required: - - form - - jsonLogicTrigger - - price - - url - - uuid FormRegistrationBackend: type: object properties: diff --git a/src/openforms/forms/api/serializers/__init__.py b/src/openforms/forms/api/serializers/__init__.py index 8e7ed98316..33d1fb91c6 100644 --- a/src/openforms/forms/api/serializers/__init__.py +++ b/src/openforms/forms/api/serializers/__init__.py @@ -5,11 +5,9 @@ from .form_variable import FormVariableListSerializer, FormVariableSerializer from .form_version import FormVersionSerializer from .logic.form_logic import FormLogicSerializer -from .logic.form_logic_price import FormPriceLogicSerializer __all__ = [ "FormLogicSerializer", - "FormPriceLogicSerializer", "FormSerializer", "FormExportSerializer", "FormImportSerializer", diff --git a/src/openforms/forms/api/serializers/logic/form_logic_price.py b/src/openforms/forms/api/serializers/logic/form_logic_price.py deleted file mode 100644 index 760a587258..0000000000 --- a/src/openforms/forms/api/serializers/logic/form_logic_price.py +++ /dev/null @@ -1,22 +0,0 @@ -from openforms.api.serializers import ListWithChildSerializer -from openforms.forms.api.serializers.logic.form_logic import FormLogicBaseSerializer -from openforms.forms.models import FormPriceLogic - - -class FormPriceLogicListSerializer(ListWithChildSerializer): - child_serializer_class = "openforms.forms.api.serializers.logic.form_logic_price.FormPriceLogicSerializer" - - -class FormPriceLogicSerializer(FormLogicBaseSerializer): - class Meta(FormLogicBaseSerializer.Meta): - model = FormPriceLogic - list_serializer_class = FormPriceLogicListSerializer - fields = FormLogicBaseSerializer.Meta.fields + ("price",) - extra_kwargs = { - **FormLogicBaseSerializer.Meta.extra_kwargs, - "url": { - "view_name": "api:form-price-logic-rules", - "lookup_field": "uuid", - "lookup_url_kwarg": "uuid_or_slug", - }, - } diff --git a/src/openforms/forms/api/viewsets.py b/src/openforms/forms/api/viewsets.py index 2f6617ad1b..e0a81e87e2 100644 --- a/src/openforms/forms/api/viewsets.py +++ b/src/openforms/forms/api/viewsets.py @@ -48,7 +48,6 @@ FormDefinitionSerializer, FormImportSerializer, FormLogicSerializer, - FormPriceLogicSerializer, FormSerializer, FormStepSerializer, FormVariableListSerializer, @@ -56,7 +55,6 @@ FormVersionSerializer, ) from .serializers.logic.form_logic import FormLogicListSerializer -from .serializers.logic.form_logic_price import FormPriceLogicListSerializer @extend_schema( @@ -294,23 +292,6 @@ def configuration(self, request: Request, *args, **kwargs): status.HTTP_405_METHOD_NOT_ALLOWED: ExceptionSerializer, }, ), - price_logic_rules_bulk_update=extend_schema( - summary=_("Bulk configure price logic rules"), - description=_( - "By sending a list of FormPriceLogic to this endpoint, all the FormPriceLogic related to the form will be " - "replaced with the data sent to the endpoint." - ), - tags=["logic-rules"], - request=FormPriceLogicListSerializer, - responses={ - status.HTTP_200_OK: FormPriceLogicListSerializer, - status.HTTP_400_BAD_REQUEST: ValidationErrorSerializer, - status.HTTP_401_UNAUTHORIZED: ExceptionSerializer, - status.HTTP_403_FORBIDDEN: ExceptionSerializer, - status.HTTP_404_NOT_FOUND: ExceptionSerializer, - status.HTTP_405_METHOD_NOT_ALLOWED: ExceptionSerializer, - }, - ), ) class FormViewSet(viewsets.ModelViewSet): """ @@ -364,8 +345,6 @@ def get_queryset(self): "variables_list", "logic_rules_bulk_update", "logic_rules_list", - "price_logic_rules_bulk_update", - "price_logic_rules_list", ): queryset = queryset.select_related(None).prefetch_related(None) @@ -615,61 +594,6 @@ def logic_rules_list(self, request, *args, **kwargs): ) return Response(serializer.data, status=status.HTTP_200_OK) - @action( - detail=True, - methods=["put"], - url_path="price-logic-rules", - url_name="price-logic-rules", - ) - @transaction.atomic - def price_logic_rules_bulk_update(self, request, *args, **kwargs): - form = self.get_object() - price_logic_rules = form.formpricelogic_set.all() - # We expect that all the price logic rules associated with a form come in the request. - # So we can delete any existing rule because they will be replaced. - price_logic_rules.delete() - - serializer = FormPriceLogicSerializer( - data=request.data, - many=True, - context={ - "request": request, - "form": form, - # context for :class:`openforms.api.fields.RelatedFieldFromContext` lookups - "forms": {str(form.uuid): form}, - "form_variables": FormVariableWrapper(form), - }, - ) - serializer.is_valid(raise_exception=True) - serializer.save() - - return Response(serializer.data, status=status.HTTP_200_OK) - - @extend_schema( - summary=_("List price logic rules"), - description=_("List all price logic rules defined for a form."), - tags=["logic-rules"], - request=FormPriceLogicListSerializer, - responses={ - status.HTTP_200_OK: FormPriceLogicListSerializer, - status.HTTP_401_UNAUTHORIZED: ExceptionSerializer, - status.HTTP_403_FORBIDDEN: ExceptionSerializer, - status.HTTP_404_NOT_FOUND: ExceptionSerializer, - status.HTTP_405_METHOD_NOT_ALLOWED: ExceptionSerializer, - }, - ) - @price_logic_rules_bulk_update.mapping.get - def price_logic_rules_list(self, request, *args, **kwargs): - form = self.get_object() - price_logic_rules = form.formpricelogic_set.all() - - serializer = FormPriceLogicSerializer( - instance=price_logic_rules, - many=True, - context={"request": request, "form": form}, - ) - return Response(serializer.data, status=status.HTTP_200_OK) - FormViewSet.__doc__ = inspect.getdoc(FormViewSet).format( admin_fields=_FORM_ADMIN_FIELDS_MARKDOWN diff --git a/src/openforms/forms/tests/test_api_form_price_logic_bulk.py b/src/openforms/forms/tests/test_api_form_price_logic_bulk.py deleted file mode 100644 index 5ad4fffd2f..0000000000 --- a/src/openforms/forms/tests/test_api_form_price_logic_bulk.py +++ /dev/null @@ -1,192 +0,0 @@ -from decimal import Decimal - -from rest_framework import status -from rest_framework.reverse import reverse, reverse_lazy -from rest_framework.test import APITestCase - -from openforms.accounts.tests.factories import SuperUserFactory, UserFactory - -from ..models import FormPriceLogic -from .factories import FormFactory, FormPriceLogicFactory - - -class FormPriceLogicBulkAPITests(APITestCase): - @classmethod - def setUpTestData(cls): - super().setUpTestData() - - cls.superuser = SuperUserFactory.create() - cls.form = FormFactory.create( - generate_minimal_setup=True, - formstep__form_definition__configuration={ - "components": [ - { - "type": "textfield", - "key": "step1_textfield1", - } - ] - }, - ) - cls.form_url = reverse( - "api:form-detail", kwargs={"uuid_or_slug": cls.form.uuid} - ) - - def test_auth_required(self): - response = self.client.get( - reverse_lazy( - "api:form-price-logic-rules", kwargs={"uuid_or_slug": self.form.uuid} - ) - ) - - self.assertEqual(status.HTTP_401_UNAUTHORIZED, response.status_code) - - def test_staff_user_required(self): - user = UserFactory.create(is_staff=False) - self.client.force_authenticate(user) - - response = self.client.get( - reverse_lazy( - "api:form-price-logic-rules", kwargs={"uuid_or_slug": self.form.uuid} - ) - ) - - self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) - - def test_list_and_filter_price_logic(self): - self.client.force_authenticate(self.superuser) - fpl1, fpl2 = FormPriceLogicFactory.create_batch(2) - assert fpl1.form != fpl2.form - - url = reverse( - "api:form-price-logic-rules", kwargs={"uuid_or_slug": fpl1.form.uuid} - ) - response = self.client.get(url) - - self.assertEqual(status.HTTP_200_OK, response.status_code) - response_data = response.json() - self.assertEqual(len(response_data), 1) - self.assertEqual(response_data[0]["uuid"], str(fpl1.uuid)) - - def test_create_price_logic(self): - self.client.force_authenticate(user=self.superuser) - price_logic_data = [ - { - "form": f"http://testserver{self.form_url}", - "json_logic_trigger": { - "==": [ - {"var": "step1_textfield1"}, - "test", - ] - }, - "price": "15.00", - } - ] - - url = reverse( - "api:form-price-logic-rules", kwargs={"uuid_or_slug": self.form.uuid} - ) - response = self.client.put(url, data=price_logic_data) - - self.assertEqual(status.HTTP_200_OK, response.status_code) - price_logics_qs = FormPriceLogic.objects.all() - self.assertEqual(price_logics_qs.count(), 1) - price_logic = price_logics_qs.get() - self.assertEqual(price_logic.form, self.form) - self.assertEqual(price_logic.price, Decimal("15.00")) - - def test_create_logic_with_dates(self): - self.client.force_authenticate(user=self.superuser) - form = FormFactory.create( - generate_minimal_setup=True, - formstep__form_definition__configuration={ - "components": [ - { - "type": "datetime", - "key": "dateOfBirth", - } - ] - }, - ) - form_url = reverse("api:form-detail", kwargs={"uuid_or_slug": form.uuid}) - price_logic_data = [ - { - "form": f"http://testserver{form_url}", - "json_logic_trigger": { - ">": [ - {"date": {"var": "dateOfBirth"}}, - {"-": [{"today": []}, {"rdelta": [18]}]}, - ] - }, - "price": "15.00", - } - ] - - url = reverse("api:form-price-logic-rules", kwargs={"uuid_or_slug": form.uuid}) - response = self.client.put(url, data=price_logic_data) - - self.assertEqual(status.HTTP_200_OK, response.status_code) - - def test_delete_price_logic(self): - self.client.force_authenticate(user=self.superuser) - price_rule = FormPriceLogicFactory.create(form__generate_minimal_setup=True) - - url = reverse( - "api:form-price-logic-rules", kwargs={"uuid_or_slug": price_rule.form.uuid} - ) - - response = self.client.put(url, data=[]) - - self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertFalse(FormPriceLogic.objects.exists()) - - def test_invalid_logic_trigger(self): - self.client.force_authenticate(user=self.superuser) - price_logic_data = [ - { - "form": f"http://testserver{self.form_url}", - "json_logic_trigger": { - "invalid_op": [ - {"var": "step1_textfield1"}, - "hide step 1", - ] - }, - "price": "123.15", - } - ] - - url = reverse( - "api:form-price-logic-rules", kwargs={"uuid_or_slug": self.form.uuid} - ) - response = self.client.put(url, data=price_logic_data) - - self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) - self.assertEqual( - response.json()["invalidParams"][0]["name"], - "0.jsonLogicTrigger", - ) - - def test_invalid_price(self): - self.client.force_authenticate(user=self.superuser) - price_logic_data = [ - { - "form": f"http://testserver{self.form_url}", - "json_logic_trigger": { - "==": [ - {"var": "step1_textfield1"}, - "hide step 1", - ] - }, - "price": "", - } - ] - - url = reverse( - "api:form-price-logic-rules", kwargs={"uuid_or_slug": self.form.uuid} - ) - response = self.client.put(url, data=price_logic_data) - - self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) - self.assertEqual( - response.json()["invalidParams"][0]["name"], - "0.price", - ) From d1c50ae2d42d47c23d37edced0363d4665b782b4 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 26 Nov 2024 08:46:50 +0100 Subject: [PATCH 06/10] :wastebasket: [#4771] 'Disable' support for evaluating price logic rules We can't effectively remove this codepath yet because it's still used in the migration tests to ensure the calculated price before and after migrating has the same outcome, rather than testing the implementation details/state of the database. API endpoints and UI are removed, so there should not be a way to be able to get new price logic rules in the system. We'll also remove admin access and support for copying them when copying a form. --- src/openforms/submissions/pricing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/openforms/submissions/pricing.py b/src/openforms/submissions/pricing.py index 173c6a25ab..7099c69016 100644 --- a/src/openforms/submissions/pricing.py +++ b/src/openforms/submissions/pricing.py @@ -2,6 +2,7 @@ import decimal import logging +import warnings from decimal import Decimal from typing import TYPE_CHECKING @@ -76,6 +77,12 @@ def get_submission_price(submission: Submission) -> Decimal: data = submission.data # test the rules one by one, if relevant price_rules = form.formpricelogic_set.all() + if price_rules: + warnings.warn( + "Price logic rules are no longer supported. The left-over implementation " + "only exists for migration testing purposes.", + RuntimeWarning, + ) for rule in price_rules: # logic does not match, no point in bothering if not jsonLogic(rule.json_logic_trigger, data): From 8059ebe1f9c7921b942336d7286737b2b9cd1907 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 26 Nov 2024 11:03:45 +0100 Subject: [PATCH 07/10] :white_check_mark: [#4771] Update tests for new price calculation behaviour There are some slight behaviour changes here that need to be documented, namely the refusal to fall back to product price if price is derived from a variable, and the fact we can no longer make some sanity check assertions in the test method. However, the behaviour is covered by the main assertion of the test. --- src/openforms/forms/models/pricing_logic.py | 10 +-- src/openforms/forms/tests/factories.py | 82 +++++++++++++++---- src/openforms/submissions/api/mixins.py | 2 +- src/openforms/submissions/api/viewsets.py | 8 +- .../submissions/models/submission.py | 2 +- .../tests/test_price_calculation.py | 33 +------- .../submissions/tests/test_read_submission.py | 27 +++--- .../tests/test_submission_completion.py | 43 ++++------ src/openforms/submissions/utils.py | 6 +- 9 files changed, 105 insertions(+), 108 deletions(-) diff --git a/src/openforms/forms/models/pricing_logic.py b/src/openforms/forms/models/pricing_logic.py index e0fcc70b88..a5a40af393 100644 --- a/src/openforms/forms/models/pricing_logic.py +++ b/src/openforms/forms/models/pricing_logic.py @@ -16,15 +16,7 @@ class FormPriceLogic(models.Model): The data model is similar to :class:`openforms.forms.models.FormLogic`. - .. todo:: - - Document logic evaluation - rules are evaluated and as soon as one matches, - that's the winner. If multiple rules match, the first one wins. - - .. todo:: - - Support complex conditions (AND/OR them together). This is a broader logic - editing/evaluation issue that applies for regular form logic too. + .. warning:: This feature is no longer supported. """ uuid = models.UUIDField(_("UUID"), unique=True, default=_uuid.uuid4) diff --git a/src/openforms/forms/tests/factories.py b/src/openforms/forms/tests/factories.py index a747d15051..73ad058afb 100644 --- a/src/openforms/forms/tests/factories.py +++ b/src/openforms/forms/tests/factories.py @@ -2,11 +2,12 @@ import factory +from openforms.forms.constants import LogicActionTypes from openforms.products.tests.factories import ProductFactory from openforms.registrations.registry import register as registration_registry from openforms.variables.constants import FormVariableDataTypes, FormVariableSources -from ..models import FormDefinition, FormStep, FormVariable +from ..models import Form, FormDefinition, FormStep, FormVariable from ..utils import form_to_json @@ -64,6 +65,45 @@ def registration_backend(form, create, extracted, **kwargs): else FormRegistrationBackendFactory.build )(form=form, backend=extracted, **kwargs) + @factory.post_generation + def price_logic( + form: Form, # pyright: ignore[reportGeneralTypeIssues] + create, + extracted, + **kwargs, + ): + """ + Configure the form to evaluate the price using logic rules and read it from + the respective variable. + + .. note:: submissions for the form must call + ``openforms.submissions.utils.persist_user_defined_variables`` + + .. note:: forms with price logic must have at least one step + """ + if not (extracted or kwargs): + return + + if not create: + raise ValueError( + "Price logic can only be specified with the create strategy" + ) + + form_logic = extracted or FormLogicFactory.create( + form=form, + json_logic_trigger=kwargs.get("json_logic_trigger", True), + for_submission_price=True, + price_variable=kwargs.get("price_variable", "totalPrice"), + price_value=kwargs.get("price_value", 5.00), + order=kwargs.get( + "order", 1000 + ), # should typically be one of the last rules evaluated + ) + variable_key = form_logic.actions[0]["variable"] + FormVariableFactory.create(form=form, for_price=True, key=variable_key) + form.price_variable_key = variable_key + form.save() + class FormRegistrationBackendFactory(factory.django.DjangoModelFactory): key = factory.Sequence(lambda n: f"backend{n}") @@ -175,26 +215,29 @@ def post(obj, create, extracted, **kwargs): class FormLogicFactory(factory.django.DjangoModelFactory): json_logic_trigger = {"==": [{"var": "test-key"}, 1]} - actions = [{"action": {"type": "disable-next"}}] class Meta: model = "forms.FormLogic" + class Params: + # generate a logic rule that sets a submission price + for_submission_price = False + price_variable = "totalPrice" + price_value = 5.00 # literal value or JSON logic expression -class FormPriceLogicFactory(factory.django.DjangoModelFactory): - form = factory.SubFactory(FormFactory) - json_logic_trigger = {"==": [{"var": "test-key"}, 1]} - price = factory.Faker( - "pydecimal", - left_digits=2, - right_digits=2, - positive=True, - min_value=5.00, - max_value=100.00, - ) - - class Meta: - model = "forms.FormPriceLogic" + @factory.lazy_attribute + def actions(self): + if self.for_submission_price: # type: ignore + return [ + { + "variable": self.price_variable, # type: ignore + "action": { + "type": LogicActionTypes.variable, + "value": self.price_value, # type: ignore + }, + } + ] + return [{"action": {"type": LogicActionTypes.disable_next}}] class FormVariableFactory(factory.django.DjangoModelFactory): @@ -213,6 +256,13 @@ class Params: source=FormVariableSources.user_defined, form_definition=None, ) + for_price = factory.Trait( + name="Total price", + key="totalPrice", + form_definition=None, + source=FormVariableSources.user_defined, + data_type=FormVariableDataTypes.float, + ) @factory.post_generation def form_definition(obj, create, extracted, **kwargs): diff --git a/src/openforms/submissions/api/mixins.py b/src/openforms/submissions/api/mixins.py index a51691cbc7..310a14a68e 100644 --- a/src/openforms/submissions/api/mixins.py +++ b/src/openforms/submissions/api/mixins.py @@ -34,7 +34,7 @@ def _complete_submission(self, submission: Submission) -> str: submission.calculate_price(save=False) submission.completed_on = timezone.now() - persist_user_defined_variables(submission, self.request) + persist_user_defined_variables(submission) # all logic has run; we can fix backend submission.save() diff --git a/src/openforms/submissions/api/viewsets.py b/src/openforms/submissions/api/viewsets.py index ebe974b574..852c96a933 100644 --- a/src/openforms/submissions/api/viewsets.py +++ b/src/openforms/submissions/api/viewsets.py @@ -122,12 +122,8 @@ class SubmissionViewSet( mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet, ): - queryset = ( - Submission.objects.select_related("form", "form__product") - .prefetch_related( - "form__formpricelogic_set", - ) - .order_by("created_on") + queryset = Submission.objects.select_related("form", "form__product").order_by( + "created_on" ) serializer_class = SubmissionSerializer authentication_classes = (AnonCSRFSessionAuthentication,) diff --git a/src/openforms/submissions/models/submission.py b/src/openforms/submissions/models/submission.py index f41ec6716f..8bb9f7a642 100644 --- a/src/openforms/submissions/models/submission.py +++ b/src/openforms/submissions/models/submission.py @@ -142,7 +142,7 @@ class Submission(models.Model): editable=False, help_text=_( "Cost of this submission. Either derived from the related product, " - "or evaluated from price logic rules. The price is calculated and saved " + "or set through logic rules. The price is calculated and saved " "on submission completion." ), ) diff --git a/src/openforms/submissions/tests/test_price_calculation.py b/src/openforms/submissions/tests/test_price_calculation.py index 93f39358a9..ffb074bbed 100644 --- a/src/openforms/submissions/tests/test_price_calculation.py +++ b/src/openforms/submissions/tests/test_price_calculation.py @@ -2,11 +2,11 @@ from django.test import TestCase -from openforms.forms.tests.factories import FormPriceLogicFactory, FormVariableFactory +from openforms.forms.tests.factories import FormVariableFactory from openforms.variables.constants import FormVariableDataTypes from ..pricing import InvalidPrice, get_submission_price -from .factories import SubmissionFactory, SubmissionStepFactory +from .factories import SubmissionFactory class PriceCalculationTests(TestCase): @@ -26,35 +26,6 @@ def test_price_from_related_product(self): self.assertEqual(price, Decimal("123.45")) - def test_price_from_logic_rules(self): - submission = SubmissionFactory.create( - form__generate_minimal_setup=True, - form__product__price=Decimal("123.45"), - form__payment_backend="demo", - form__price_variable_key="", - ) - FormVariableFactory.create( - key="test-key", - form=submission.form, - form_definition=submission.form.formstep_set.get().form_definition, - ) - SubmissionStepFactory.create( - submission=submission, - form_step=submission.form.formstep_set.get(), - data={"test-key": "test"}, - ) - FormPriceLogicFactory.create( - form=submission.form, - json_logic_trigger={"==": [{"var": "test-key"}, "test"]}, - price=Decimal("51.15"), - ) - with self.subTest(part="check data setup"): - self.assertTrue(submission.payment_required) - - price = get_submission_price(submission) - - self.assertEqual(price, Decimal("51.15")) - def test_price_from_form_variable(self): submission = SubmissionFactory.create( completed=True, diff --git a/src/openforms/submissions/tests/test_read_submission.py b/src/openforms/submissions/tests/test_read_submission.py index fe767c4d18..7f95237201 100644 --- a/src/openforms/submissions/tests/test_read_submission.py +++ b/src/openforms/submissions/tests/test_read_submission.py @@ -18,13 +18,13 @@ from openforms.forms.tests.factories import ( FormFactory, FormLogicFactory, - FormPriceLogicFactory, FormStepFactory, FormVariableFactory, ) from openforms.logging.models import TimelineLogProxy from openforms.variables.constants import FormVariableDataTypes, FormVariableSources +from ..utils import persist_user_defined_variables from .factories import ( SubmissionFactory, SubmissionStepFactory, @@ -193,6 +193,9 @@ def test_submission_payment_information_uses_logic_rules(self): form__generate_minimal_setup=True, form__product__price=Decimal("123.45"), form__payment_backend="demo", + form__price_logic__price_variable="totalPrice", + form__price_logic__price_value=51.15, + form__price_logic__json_logic_trigger={"==": [{"var": "test-key"}, "test"]}, ) FormVariableFactory.create( key="test-key", @@ -204,11 +207,7 @@ def test_submission_payment_information_uses_logic_rules(self): form_step=submission.form.formstep_set.get(), data={"test-key": "test"}, ) - FormPriceLogicFactory.create( - form=submission.form, - json_logic_trigger={"==": [{"var": "test-key"}, "test"]}, - price=Decimal("51.15"), - ) + persist_user_defined_variables(submission) submission.calculate_price() with self.subTest(part="check data setup"): self.assertTrue(submission.payment_required) @@ -270,6 +269,11 @@ def test_submission_payment_with_logic_using_user_defined_variables(self): submitted_data={"triggerComponent": 1}, form__product__price=Decimal("10"), form__payment_backend="demo", + form__price_logic__price_variable="totalPrice", + form__price_logic__json_logic_trigger={ + "==": [{"var": "userDefinedVar"}, 2] + }, + form__price_logic__price_value=20, ) SubmissionValueVariableFactory.create( key="userDefinedVar", @@ -288,15 +292,10 @@ def test_submission_payment_with_logic_using_user_defined_variables(self): "action": {"type": "variable", "value": 2}, } ], + order=1, ) - FormPriceLogicFactory.create( - form=submission.form, - json_logic_trigger={"==": [{"var": "userDefinedVar"}, 2]}, - price=Decimal("20"), - ) - - self.assertTrue(submission.payment_required) - + persist_user_defined_variables(submission) + assert submission.payment_required self._add_submission_to_session(submission) endpoint = reverse("api:submission-detail", kwargs={"uuid": submission.uuid}) diff --git a/src/openforms/submissions/tests/test_submission_completion.py b/src/openforms/submissions/tests/test_submission_completion.py index 4823122e9a..da68c84602 100644 --- a/src/openforms/submissions/tests/test_submission_completion.py +++ b/src/openforms/submissions/tests/test_submission_completion.py @@ -25,7 +25,6 @@ from openforms.forms.tests.factories import ( FormFactory, FormLogicFactory, - FormPriceLogicFactory, FormRegistrationBackendFactory, FormStepFactory, FormVariableFactory, @@ -34,6 +33,7 @@ from openforms.registrations.base import BasePlugin from openforms.registrations.registry import Registry from openforms.registrations.tests.utils import patch_registry +from openforms.submissions.pricing import InvalidPrice from openforms.variables.constants import FormVariableDataTypes, FormVariableSources from ..constants import SUBMISSIONS_SESSION_KEY, PostSubmissionEvents @@ -667,17 +667,15 @@ def test_no_product_linked_but_price_rules_set(self): form__generate_minimal_setup=True, form__product=None, form__payment_backend="demo", + form__price_logic__price_variable="totalPrice", + form__price_logic__price_value=9.6, + form__price_logic__json_logic_trigger={"==": [{"var": "test-key"}, "test"]}, ) SubmissionStepFactory.create( submission=submission, form_step=submission.form.formstep_set.get(), data={"test-key": "test"}, ) - FormPriceLogicFactory.create( - form=submission.form, - json_logic_trigger={"==": [{"var": "test-key"}, "test"]}, - price=Decimal("9.6"), - ) with self.subTest(part="check data setup"): self.assertFalse(submission.payment_required) self._add_submission_to_session(submission) @@ -718,6 +716,9 @@ def test_price_rules_specified(self): form__generate_minimal_setup=True, form__product__price=Decimal("123.45"), form__payment_backend="demo", + form__price_logic__price_variable="totalPrice", + form__price_logic__price_value=51.15, + form__price_logic__json_logic_trigger={"==": [{"var": "test-key"}, "test"]}, ) FormVariableFactory.create( key="test-key", @@ -729,13 +730,6 @@ def test_price_rules_specified(self): form_step=submission.form.formstep_set.get(), data={"test-key": "test"}, ) - FormPriceLogicFactory.create( - form=submission.form, - json_logic_trigger={"==": [{"var": "test-key"}, "test"]}, - price=Decimal("51.15"), - ) - with self.subTest(part="check data setup"): - self.assertTrue(submission.payment_required) self._add_submission_to_session(submission) endpoint = reverse("api:submission-complete", kwargs={"uuid": submission.uuid}) @@ -748,12 +742,18 @@ def test_price_rules_specified(self): def test_price_rules_specified_but_no_match(self): """ - Assert that the product price is used as fallback. + When there is ambiguity, we bail out instead of guessing/falling back to + product price. """ submission = SubmissionFactory.create( form__generate_minimal_setup=True, form__product__price=Decimal("123.45"), form__payment_backend="demo", + form__price_logic__price_variable="totalPrice", + form__price_logic__price_value=51.15, + form__price_logic__json_logic_trigger={ + "==": [{"var": "test-key"}, "nottest"] + }, ) FormVariableFactory.create( key="test-key", @@ -765,22 +765,11 @@ def test_price_rules_specified_but_no_match(self): form_step=submission.form.formstep_set.get(), data={"test-key": "test"}, ) - FormPriceLogicFactory.create( - form=submission.form, - json_logic_trigger={"==": [{"var": "test-key"}, "nottest"]}, - price=Decimal("51.15"), - ) - with self.subTest(part="check data setup"): - self.assertTrue(submission.payment_required) self._add_submission_to_session(submission) endpoint = reverse("api:submission-complete", kwargs={"uuid": submission.uuid}) - response = self.client.post(endpoint, {"privacy_policy_accepted": True}) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - submission.refresh_from_db() - self.assertTrue(submission.payment_required) - self.assertEqual(submission.price, Decimal("123.45")) + with self.assertRaises(InvalidPrice): + self.client.post(endpoint, {"privacy_policy_accepted": True}) @override_settings(CELERY_TASK_ALWAYS_EAGER=True) diff --git a/src/openforms/submissions/utils.py b/src/openforms/submissions/utils.py index 6d8d1e8b52..c88100587e 100644 --- a/src/openforms/submissions/utils.py +++ b/src/openforms/submissions/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from contextlib import contextmanager from typing import Any @@ -239,9 +241,7 @@ def initialise_user_defined_variables(submission: Submission): ) -def persist_user_defined_variables( - submission: "Submission", request: "Request" -) -> None: +def persist_user_defined_variables(submission: Submission) -> None: data = submission.data last_form_step = submission.submissionstep_set.order_by("form_step__order").last() From dfa3f568b31e4e82b3883abdb97998320797d12f Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 26 Nov 2024 11:32:03 +0100 Subject: [PATCH 08/10] :pencil: [#4771] Document 3.0 breaking changes w/r to price logic ... and other things that should've had their documentation added already but I neglected to do so. --- docs/installation/index.rst | 4 +- docs/installation/upgrade-300.rst | 70 ++++++++++++++++++++++++++----- docs/manual/forms/basics.rst | 9 ++-- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 1789b0b16e..f3de65fdea 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -29,6 +29,6 @@ and expertise. form_hosting redis issues/index - upgrade-240 - upgrade-250 upgrade-300 + upgrade-250 + upgrade-240 diff --git a/docs/installation/upgrade-300.rst b/docs/installation/upgrade-300.rst index e8c34854c5..c1594a2c4d 100644 --- a/docs/installation/upgrade-300.rst +++ b/docs/installation/upgrade-300.rst @@ -4,16 +4,66 @@ Upgrade details to Open Forms 3.0.0 =================================== -Backwards compatibility in form import changes -============================================== -Removal of Object API object type convertion --------------------------------------------- +Open Forms 3.0 is a major version release that contains some breaking changes. As always +we try to limit the impact of breaking changes through automatic migrations, and this +release is no different, but there are some subtle changes in behaviour that you should +be aware of, as they may require additional manual actions. -With the UX changes of OF 2.8.0, the Object Api registration no longer lets you use -hyperlinks when configuring the object type. The usage of hyperlinks for the object type -is now also disallowed when importing a form. +.. contents:: Jump to + :depth: 1 + :local: -Previously the hyperlinks would be converted to the expected format, and saved as such. -The convertion will no-longer take place, and the 'to be imported' form is expected to -use the new UUID format for the object type. +Removal of price logic +====================== + +Price logic rules are removed in favour of setting the submission price via a form +variable and normal logic rules. The conversion is automatic. + +There is a slight change in behaviour. When no price logic rules matched the trigger +condition, the price set on the related product was used. This can lead to surprises +and wrong amounts being paid due to logical errors in the form itself. + +The new behaviour will (deliberately) cause a crash that will show to the end-user +as "something unexpectedly went wrong", since we refuse to make any (likely wrong) +assumptions about the amount that needs to be paid. Crash information is visible +in the error monitoring if that's set up correctly. + +.. note:: Existing, automatically converted forms are crash-free because we create an + explicit fallback logic rule that mimicks the old behaviour. + +Form components/fields changes +============================== + +Password component removed +-------------------------- + +The password component was deprecated a long time ago, and has now been removed. If you +need to replace it anywhere, use a regular textfield component instead, but you really +shouldn't be asking users for passwords. + +Removed import conversions +========================== + +For a number of changes, Open Forms ensured that old form exports could still be +imported and would automatically convert some data. Some of these conversion have been +removed. + +Removal of objecttype URL conversion in the Objects API registration options +---------------------------------------------------------------------------- + +Since the UX improvments in Open Forms 2.8.0 you can select the object type in a +dropdown, and under the hood we save the unique identifier rather than the full API +resource URL (which you used to have to copy-paste in a text field). The usage of +hyperlinks for the object type is now also disallowed when importing a form. + +Previously the hyperlinks would be converted to the expected format, and saved as such, +which was quite complex and not ideal for exports using the new format. We +recommend re-creating the exports on a newer version of Open Forms. + +Removal of legacy translations conversion +----------------------------------------- + +Old (from before Open Forms 2.4) form export files containing form field translations +in the legacy format are now ignored instead of converted to the new format. We +recommend re-creating the exports on a newer version of Open Forms. diff --git a/docs/manual/forms/basics.rst b/docs/manual/forms/basics.rst index 89b0ffb8bd..1c580737d2 100644 --- a/docs/manual/forms/basics.rst +++ b/docs/manual/forms/basics.rst @@ -199,7 +199,7 @@ product bevat een prijs die gebruikt kan worden als betaald moet worden voor het product. Betaling kan ingesteld worden door de juiste **Betaalprovider** te selecteren. -Er zijn drie manieren om de prijs van een inzending te bepalen: +Er zijn twee manieren om de prijs van een inzending te bepalen: **Gebruik de prijs van het gekoppeld product** @@ -226,10 +226,9 @@ te activeren. **Gebruik prijslogica** -Voor eenvoudige condities kan je prijslogic instellen. Onder een bepaalde conditie geldt -een bepaalde, vaste, prijs. Indien aan geen enkele conditie voldaan is, dan wordt de -prijs van het gekoppeld product gebruikt. De **Prijslogica** volgt verder dezelfde -regels als reguliere **Logica**. +.. versionremoved:: 3.0 + + De prijslogica is vervangen door gewone logica + gebruik van een variabele. Zie ook: :ref:`configuration_payment_index` From 1b0bc4bd5ded01c740f9a112e9c04862496464a7 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 26 Nov 2024 11:35:46 +0100 Subject: [PATCH 09/10] :card_file_box: [#4771] Migration for updated help text --- .../migrations/0012_alter_submission_price.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/openforms/submissions/migrations/0012_alter_submission_price.py diff --git a/src/openforms/submissions/migrations/0012_alter_submission_price.py b/src/openforms/submissions/migrations/0012_alter_submission_price.py new file mode 100644 index 0000000000..cc7276b2b3 --- /dev/null +++ b/src/openforms/submissions/migrations/0012_alter_submission_price.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.16 on 2024-11-26 10:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("submissions", "0011_remove_submissionstep__data"), + ] + + operations = [ + migrations.AlterField( + model_name="submission", + name="price", + field=models.DecimalField( + blank=True, + decimal_places=2, + editable=False, + help_text="Cost of this submission. Either derived from the related product, or set through logic rules. The price is calculated and saved on submission completion.", + max_digits=10, + null=True, + verbose_name="price", + ), + ), + ] From bf46b3ef43916b911451d0e6f6ff6eaea1a9c4e8 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 26 Nov 2024 11:42:29 +0100 Subject: [PATCH 10/10] :globe_with_meridians: [#4771] Update translation catalogs --- src/openforms/js/compiled-lang/en.json | 24 ------------------------ src/openforms/js/compiled-lang/nl.json | 24 ------------------------ src/openforms/js/lang/en.json | 20 -------------------- src/openforms/js/lang/nl.json | 20 -------------------- 4 files changed, 88 deletions(-) diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index c0c5ce575d..f0f116a9aa 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -1383,12 +1383,6 @@ "value": "plus" } ], - "BrsG4w": [ - { - "type": 0, - "value": "Then the price is" - } - ], "BvRBef": [ { "type": 0, @@ -2909,12 +2903,6 @@ "value": "Output mapping" } ], - "QAbqb6": [ - { - "type": 0, - "value": "Add rule" - } - ], "QDBI2H": [ { "type": 0, @@ -4439,12 +4427,6 @@ "value": "Attachments" } ], - "eJEu3L": [ - { - "type": 0, - "value": "Are you sure you want to delete this rule?" - } - ], "eLDxD5": [ { "type": 0, @@ -6665,12 +6647,6 @@ "value": "Plugin configuration: Ogone legacy" } ], - "zeXZzX": [ - { - "type": 0, - "value": "Use logic rules to determine the price" - } - ], "zwOwrD": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index 6b5d2f2d9f..6e50fb8430 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -1387,12 +1387,6 @@ "value": "plus" } ], - "BrsG4w": [ - { - "type": 0, - "value": "dan is de prijs" - } - ], "BvRBef": [ { "type": 0, @@ -2926,12 +2920,6 @@ "value": "Uitvoerparameters" } ], - "QAbqb6": [ - { - "type": 0, - "value": "Regel toevoegen" - } - ], "QDBI2H": [ { "type": 0, @@ -4461,12 +4449,6 @@ "value": "Bijlagen" } ], - "eJEu3L": [ - { - "type": 0, - "value": "Weet u zeker dat u deze regel wil verwijderen?" - } - ], "eLDxD5": [ { "type": 0, @@ -6693,12 +6675,6 @@ "value": "Plugin-instellingen: Ogone legacy" } ], - "zeXZzX": [ - { - "type": 0, - "value": "Gebruik prijsregels om de prijs te bepalen" - } - ], "zwOwrD": [ { "type": 0, diff --git a/src/openforms/js/lang/en.json b/src/openforms/js/lang/en.json index 6e4c1ae471..0c535db7ce 100644 --- a/src/openforms/js/lang/en.json +++ b/src/openforms/js/lang/en.json @@ -614,11 +614,6 @@ "description": "\"+\" operator description", "originalDefault": "plus" }, - "BrsG4w": { - "defaultMessage": "Then the price is", - "description": "Price logic prefix", - "originalDefault": "Then the price is" - }, "C+91tl": { "defaultMessage": "Data extraction", "description": "Service fetch configuration modal data extraction fieldset title", @@ -1449,11 +1444,6 @@ "description": "Output mapping title", "originalDefault": "Output mapping" }, - "QAbqb6": { - "defaultMessage": "Add rule", - "description": "Add price logic rule button", - "originalDefault": "Add rule" - }, "QDBI2H": { "defaultMessage": "Select existing form definition", "description": "Select form definition tile", @@ -2094,11 +2084,6 @@ "description": "Email registration: attachments fieldset title", "originalDefault": "Attachments" }, - "eJEu3L": { - "defaultMessage": "Are you sure you want to delete this rule?", - "description": "Price rule deletion confirm message", - "originalDefault": "Are you sure you want to delete this rule?" - }, "eLDxD5": { "defaultMessage": "Advanced options", "description": "Logic rule advanced options icon title", @@ -3129,11 +3114,6 @@ "description": "Ogone legacy options modal title", "originalDefault": "Plugin configuration: Ogone legacy" }, - "zeXZzX": { - "defaultMessage": "Use logic rules to determine the price", - "description": "dynamic pricing mode label", - "originalDefault": "Use logic rules to determine the price" - }, "zwOwrD": { "defaultMessage": "Select a property from the object type", "description": "Prefill / Objects API: accessible label for object type property selection", diff --git a/src/openforms/js/lang/nl.json b/src/openforms/js/lang/nl.json index 9864bf1f5f..d078487879 100644 --- a/src/openforms/js/lang/nl.json +++ b/src/openforms/js/lang/nl.json @@ -620,11 +620,6 @@ "isTranslated": true, "originalDefault": "plus" }, - "BrsG4w": { - "defaultMessage": "dan is de prijs", - "description": "Price logic prefix", - "originalDefault": "Then the price is" - }, "C+91tl": { "defaultMessage": "Gegevensverwerking", "description": "Service fetch configuration modal data extraction fieldset title", @@ -1464,11 +1459,6 @@ "description": "Output mapping title", "originalDefault": "Output mapping" }, - "QAbqb6": { - "defaultMessage": "Regel toevoegen", - "description": "Add price logic rule button", - "originalDefault": "Add rule" - }, "QDBI2H": { "defaultMessage": "Selecteer een bestaande formulierdefinitie", "description": "Select form definition tile", @@ -2114,11 +2104,6 @@ "description": "Email registration: attachments fieldset title", "originalDefault": "Attachments" }, - "eJEu3L": { - "defaultMessage": "Weet u zeker dat u deze regel wil verwijderen?", - "description": "Price rule deletion confirm message", - "originalDefault": "Are you sure you want to delete this rule?" - }, "eLDxD5": { "defaultMessage": "Geavanceerde opties", "description": "Logic rule advanced options icon title", @@ -3151,11 +3136,6 @@ "description": "Ogone legacy options modal title", "originalDefault": "Plugin configuration: Ogone legacy" }, - "zeXZzX": { - "defaultMessage": "Gebruik prijsregels om de prijs te bepalen", - "description": "dynamic pricing mode label", - "originalDefault": "Use logic rules to determine the price" - }, "zwOwrD": { "defaultMessage": "Selecteer een attribuut uit het objecttype", "description": "Prefill / Objects API: accessible label for object type property selection",