diff --git a/source/components/ComparativeTargets.js b/source/components/ComparativeTargets.js index ec15ee93b..6a09c62b2 100644 --- a/source/components/ComparativeTargets.js +++ b/source/components/ComparativeTargets.js @@ -11,7 +11,7 @@ import { config } from 'react-spring' import { branchAnalyseSelector } from 'Selectors/analyseSelectors' import { règleAvecMontantSelector } from 'Selectors/regleSelectors' import Animate from 'Ui/animate' -import { validInputEnteredSelector } from '../selectors/analyseSelectors' +import { noUserInputSelector } from '../selectors/analyseSelectors' import './ComparativeTargets.css' import SchemeCard from './ui/SchemeCard' @@ -22,7 +22,7 @@ const connectRègles = (situationBranchName: string) => state => { return ({ revenuDisponible: - validInputEnteredSelector(state) && + !noUserInputSelector(state) && règleAvecMontantSelector(state, { situationBranchName })('revenu net') diff --git a/source/components/QuickLinks.js b/source/components/QuickLinks.js index 57f1dd14e..8e7f6bfb0 100644 --- a/source/components/QuickLinks.js +++ b/source/components/QuickLinks.js @@ -7,7 +7,7 @@ import { Trans } from 'react-i18next' import { connect } from 'react-redux' import { withRouter } from 'react-router' import { animated, Spring } from 'react-spring' -import { validInputEnteredSelector } from 'Selectors/analyseSelectors' +import { noUserInputSelector } from 'Selectors/analyseSelectors' import type { Location } from 'react-router' type OwnProps = { @@ -16,17 +16,17 @@ type OwnProps = { type Props = OwnProps & { startConversation: (?string) => void, location: Location, - validInputEntered: boolean, + userInput: boolean, conversationStarted: boolean } const QuickLinks = ({ startConversation, - validInputEntered, + userInput, quickLinks, conversationStarted }: Props) => { - const show = validInputEntered && !conversationStarted + const show = userInput && !conversationStarted return ( ({ key: props.language, - validInputEntered: validInputEnteredSelector(state), + userInput: !noUserInputSelector(state), conversationStarted: state.conversationStarted, quickLinks: state.simulation?.config["questions à l'affiche"] }), diff --git a/source/components/SalaryExplanation.js b/source/components/SalaryExplanation.js index 08c3d5a49..853ef19b6 100644 --- a/source/components/SalaryExplanation.js +++ b/source/components/SalaryExplanation.js @@ -1,14 +1,14 @@ -import { startConversation } from 'Actions/actions'; -import withTracker from 'Components/utils/withTracker'; -import { compose } from 'ramda'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { formValueSelector } from 'redux-form'; -import ficheDePaieSelectors from 'Selectors/ficheDePaieSelectors'; -import * as Animate from 'Ui/animate'; -import SalaryCompactExplanation from './SalaryCompactExplanation'; -import './SalaryCompactExplanation.css'; -import SalaryFirstExplanation from './SalaryFirstExplanation'; +import { startConversation } from 'Actions/actions' +import withTracker from 'Components/utils/withTracker' +import { compose } from 'ramda' +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { formValueSelector } from 'redux-form' +import ficheDePaieSelectors from 'Selectors/ficheDePaieSelectors' +import * as Animate from 'Ui/animate' +import SalaryCompactExplanation from './SalaryCompactExplanation' +import './SalaryCompactExplanation.css' +import SalaryFirstExplanation from './SalaryFirstExplanation' export default compose( withTracker, diff --git a/source/components/Simulation.js b/source/components/Simulation.js index 860ca6bc5..e7bb9e6ed 100644 --- a/source/components/Simulation.js +++ b/source/components/Simulation.js @@ -10,10 +10,8 @@ import withColours from 'Components/utils/withColours' import { compose } from 'ramda' import { connect } from 'react-redux' import { - blockingInputControlsSelector, nextStepsSelector, - noUserInputSelector, - validInputEnteredSelector + noUserInputSelector } from 'Selectors/analyseSelectors' import Animate from 'Ui/animate' @@ -24,12 +22,8 @@ export default compose( conversationStarted: state.conversationStarted, previousAnswers: state.conversationSteps.foldedSteps, noNextSteps: - state.conversationStarted && - !blockingInputControlsSelector(state) && - nextStepsSelector(state).length == 0, - noUserInput: noUserInputSelector(state), - blockingInputControls: blockingInputControlsSelector(state), - validInputEntered: validInputEnteredSelector(state) + state.conversationStarted && nextStepsSelector(state).length == 0, + noUserInput: noUserInputSelector(state) }), { resetSimulation } ) @@ -46,14 +40,12 @@ export default compose( conversationStarted, resetSimulation, noFeedback, - blockingInputControls, showTargetsAnyway, targetsTriggerConversation } = this.props let arePreviousAnswers = previousAnswers.length > 0, displayConversation = - (!targetsTriggerConversation || conversationStarted) && - !blockingInputControls, + !targetsTriggerConversation || conversationStarted, showTargets = targetsTriggerConversation || !noUserInput || showTargetsAnyway return ( diff --git a/source/components/TargetSelection.js b/source/components/TargetSelection.js index 2b5d181ab..69a83e30e 100644 --- a/source/components/TargetSelection.js +++ b/source/components/TargetSelection.js @@ -5,7 +5,7 @@ import withColours from 'Components/utils/withColours' import withLanguage from 'Components/utils/withLanguage' import withSitePaths from 'Components/utils/withSitePaths' import { encodeRuleName, findRuleByDottedName } from 'Engine/rules' -import { compose, propEq } from 'ramda' +import { compose, propEq, chain } from 'ramda' import React, { Component } from 'react' import { withTranslation } from 'react-i18next' import { connect } from 'react-redux' @@ -14,7 +14,6 @@ import { Link } from 'react-router-dom' import { change, Field, formValueSelector, reduxForm } from 'redux-form' import { analysisWithDefaultsSelector, - blockingInputControlsSelector, flatRulesSelector, nextStepsSelector, noUserInputSelector @@ -40,7 +39,6 @@ export default compose( getTargetValue: dottedName => formValueSelector('conversation')(state, dottedName), analysis: analysisWithDefaultsSelector(state), - blockingInputControls: blockingInputControlsSelector(state), flatRules: flatRulesSelector(state), progress: (100 * (MAX_NUMBER_QUESTION - nextStepsSelector(state))) / @@ -65,7 +63,9 @@ export default compose( return (
- + contrôles, analysis.cache)} + />
@@ -95,7 +95,6 @@ export default compose( setActiveInput, analysis, noUserInput, - blockingInputControls, match } = this.props, targets = analysis ? analysis.targets : [] @@ -113,8 +112,7 @@ export default compose( match, target, conversationStarted, - isActiveInput: activeInput === target.dottedName, - blockingInputControls + isActiveInput: activeInput === target.dottedName }} /> {!target.question && ( @@ -133,8 +131,7 @@ export default compose( activeInput, setActiveInput, setFormValue: this.props.setFormValue, - noUserInput, - blockingInputControls + noUserInput }} />
@@ -190,15 +187,7 @@ let CurrencyField = withColours(props => { }) let TargetInputOrValue = withLanguage( - ({ - target, - targets, - activeInput, - setActiveInput, - language, - noUserInput, - blockingInputControls - }) => ( + ({ target, targets, activeInput, setActiveInput, language, noUserInput }) => ( {activeInput === target.dottedName ? ( )} @@ -230,7 +218,7 @@ const TargetValue = connect( )( class TargetValue extends Component { render() { - let { targets, target, noUserInput, blockingInputControls } = this.props + let { targets, target, noUserInput } = this.props let targetWithValue = targets && targets.find(propEq('dottedName', target.dottedName)), @@ -240,8 +228,7 @@ const TargetValue = connect(
rawNode => { export let treatRuleRoot = (rules, rule) => { /* - La fonction treatRuleRoot va descendre l'arbre de la règle `rule` et produire un AST, un objet contenant d'autres objets contenant d'autres objets... - Aujourd'hui, une règle peut avoir (comme propriétés à parser) `non applicable si`, `applicable si` et `formule`, - qui ont elles-mêmes des propriétés de type mécanisme (ex. barème) ou des expressions en ligne (ex. maVariable + 3). - Ces mécanismes ou variables sont descendues à leur tour grâce à `treat()`. - Lors de ce traitement, des fonctions 'evaluate' et `jsx` sont attachés aux objets de l'AST. Elles seront exécutées à l'évaluation. - */ + The treatRuleRoot function will traverse the tree of the `rule` and produce an AST, an object containing other objects containing other objects... + Some of the attributes of the rule are dynamic, they need to be parsed. It is the case of `non applicable si`, `applicable si`, `formule`, `contrôles`. + These attributes' values themselves may have mechanism properties (e. g. `barème`) or inline expressions (e. g. `maVariable + 3`). + These mechanisms or variables are in turn traversed by `treat()`. During this processing, 'evaluate' and'jsx' functions are attached to the objects of the AST. They will be evaluated during the evaluation phase, called "analyse". +*/ + let evaluate = (cache, situationGate, parsedRules, node) => { // console.log((cache.op || ">").padStart(cache.parseLevel),rule.dottedName) cache.parseLevel++ @@ -138,6 +138,11 @@ export let treatRuleRoot = (rules, rule) => { nodeValue } = evaluatedFormula + // if isApplicable === true + // evaluateControls + // attache them to the node for further usage + // do not output missingVariables for now + let condMissing = isApplicable === false ? {} @@ -154,6 +159,20 @@ export let treatRuleRoot = (rules, rule) => { formulaMissingVariables ) + let evaluateControls = node.contrôles && val(parentDependency) !== false + console.log(node.name, evaluateControls) + let contrôles = + evaluateControls && + node.contrôles.map(control => ({ + ...control, + evaluated: evaluateNode( + cache, + situationGate, + parsedRules, + control.testExpression + ) + })) + cache.parseLevel-- // if (keys(condMissing).length) console.log("".padStart(cache.parseLevel-1),{conditions:condMissing, formule:formMissing}) // else console.log("".padStart(cache.parseLevel-1),{formule:formMissing}) @@ -161,6 +180,7 @@ export let treatRuleRoot = (rules, rule) => { ...node, ...evaluatedAttributes, ...{ formule: evaluatedFormula }, + contrôles, nodeValue, isApplicable, missingVariables @@ -235,56 +255,23 @@ export let treatRuleRoot = (rules, rule) => { explanation: child } }, - contrôles: list => - list.map(control => { - let testExpression = treat(rules, rule)(control.si) - - return { - dottedName: rule.dottedName, - level: control['niveau'], - test: control['si'], - message: control['message'], - testExpression, - solution: control['solution'] - } - }) - })(root) - - let controls = - rule['contrôles'] && - rule['contrôles'].map(control => { + contrôles: map(control => { let testExpression = treat(rules, rule)(control.si) if (!testExpression.explanation) throw new Error( 'Ce contrôle ne semble pas être compris :' + control['si'] ) - let otherVariables = testExpression.explanation.filter( - node => - node.category === 'variable' && node.dottedName !== rule.dottedName - ) - let isInputControl = !otherVariables.length, - level = control['niveau'] - - if (level === 'bloquant' && !isInputControl) { - throw new Error( - `Un contrôle ne peut être bloquant et invoquer des calculs de variables : - ${control['si']} - ${level} - ` - ) - } - return { dottedName: rule.dottedName, level: control['niveau'], test: control['si'], message: control['message'], testExpression, - solution: control['solution'], - isInputControl + solution: control['solution'] } }) + })(root) return { // Pas de propriété explanation et jsx ici car on est parti du (mauvais) principe que 'non applicable si' et 'formule' sont particuliers, alors qu'ils pourraient être rangé avec les autres mécanismes diff --git a/source/engine/treatVariable.js b/source/engine/treatVariable.js index a0705af66..761562c85 100644 --- a/source/engine/treatVariable.js +++ b/source/engine/treatVariable.js @@ -25,10 +25,11 @@ export let treatVariable = (rules, rule, filter) => parseResult => { variable['non applicable si'] != null, situationValue = getSituationValue(situation, dottedName, variable), needsEvaluation = - situationValue == null && - (variableHasCond || - variableHasFormula || - findParentDependency(rules, variable)) + variable['contrôles'] || + (situationValue == null && + (variableHasCond || + variableHasFormula || + findParentDependency(rules, variable))) // if (dottedName.includes('jeune va')) debugger @@ -160,6 +161,7 @@ export let treatVariableTransforms = (rules, rule) => parseResult => { ruleToTransform.période === 'flexible' ? environmentPeriod : ruleToTransform.période + let transformedNodeValue = callingPeriod === 'mois' && calledPeriod === 'année' ? nodeValue / 12 diff --git a/source/selectors/analyseSelectors.js b/source/selectors/analyseSelectors.js index f4c8b10f4..88f413b93 100644 --- a/source/selectors/analyseSelectors.js +++ b/source/selectors/analyseSelectors.js @@ -217,15 +217,6 @@ let analysisValidatedOnlySelector = makeAnalysisSelector( validatedSituationBranchesSelector ) -export let blockingInputControlsSelector = state => { - let analysis = analysisWithDefaultsSelector(state) - return analysis && analysis.blockingInputControls -} - -export let validInputEnteredSelector = createSelector( - [noUserInputSelector, blockingInputControlsSelector], - (noUserInput, blockingInputControls) => !noUserInput && !blockingInputControls -) // TODO this should really not be fired twice in a user session... // // TODO the just input salary should be in the situation so that it is not a missing variable diff --git a/source/selectors/progressSelectors.js b/source/selectors/progressSelectors.js index 850e81ab9..542fa3dc8 100644 --- a/source/selectors/progressSelectors.js +++ b/source/selectors/progressSelectors.js @@ -1,7 +1,6 @@ /* @flow */ import { createSelector, createStructuredSelector } from 'reselect' import { - blockingInputControlsSelector, nextStepsSelector, noUserInputSelector } from 'Selectors/analyseSelectors' @@ -45,10 +44,7 @@ const NUMBER_MAX_QUESTION_SIMULATION = 18 const START_SIMULATION_COEFFICIENT = 0.15 const QUESTIONS_COEFFICIENT = 0.85 export const estimationProgressSelector = (state: any) => { - const userInputProgress = +( - !noUserInputSelector(state) && - !softCatch(blockingInputControlsSelector)(state) - ) + const userInputProgress = !noUserInputSelector(state) const questionsProgress = (state.conversationStarted && NUMBER_MAX_QUESTION_SIMULATION - nextStepsSelector(state).length) / diff --git a/source/selectors/regleSelectors.js b/source/selectors/regleSelectors.js index 462652a8e..94598b86e 100644 --- a/source/selectors/regleSelectors.js +++ b/source/selectors/regleSelectors.js @@ -72,6 +72,8 @@ export const règleValeurSelector: InputSelector< (analysis.cache[dottedName] || analysis.targets.find(target => target.dottedName === dottedName)) + if (rule == undefined) return null + let valeur = rule && !isNil(rule.nodeValue) ? rule.nodeValue @@ -103,6 +105,7 @@ export const règleValeurSelector: InputSelector< (!Number.isNaN(valeur) && Number.isNaN(Number.parseFloat(valeur)) ? 'string' : 'number') + return { type, valeur: diff --git a/test/contrôles.test.js b/test/contrôles.test.js index 7a160f573..3ceb10834 100644 --- a/test/contrôles.test.js +++ b/test/contrôles.test.js @@ -1,6 +1,7 @@ import { expect } from 'chai' import { enrichRule } from '../source/engine/rules' import { analyseMany, parseAll } from '../source/engine/traverse' +import { chain, values, prop } from 'ramda' describe('controls', function() { let rawRules = [ @@ -50,33 +51,16 @@ describe('controls', function() { parsedRules = parseAll(rules) it('Should parse blocking controls', function() { - let controls = parsedRules.find(r => r.controls).controls + let controls = parsedRules.find(r => r.contrôles).contrôles expect( - controls.filter( - ({ level, isInputControl }) => level == 'bloquant' && isInputControl - ) + controls.filter(({ level }) => level == 'bloquant') ).to.have.lengthOf(2) }) - it('Should block the engine evaluation if blocking input controls trigger', function() { - let situationGate = dottedName => ({ brut: 400 }[dottedName]), - { blockingInputControls } = analyseMany(parsedRules, ['net'])( - situationGate - ) - - expect(blockingInputControls).to.have.lengthOf(1) - }) - it('Should not block the engine evaluation if no blocking input controls trigger', function() { - let situationGate = dottedName => ({ brut: 1200 }[dottedName]), - { blockingInputControls } = analyseMany(parsedRules, ['net'])( - situationGate - ) - - expect(blockingInputControls).to.be.undefined - }) it('Should allow imbricated conditions', function() { let situationGate = dottedName => ({ brut: 2000000 }[dottedName]), - { controls } = analyseMany(parsedRules, ['net'])(situationGate) + cache = analyseMany(parsedRules, ['net'])(situationGate).cache, + controls = chain(prop('contrôles'), values(cache)) expect( controls.find(