Tentative d'implémentation des contrôles à l'évaluation

Evidememnt, ça pose problème : on n'a pas encore retourné la valeur d'un
noeud (ex. cotisation X) qu'on demande son évaluation dans le contrôle
(cotisation X > 450)
pull/492/head
Mael 2019-02-12 15:59:01 +01:00 committed by Johan Girod
parent c36462bd41
commit f9580f15b5
11 changed files with 76 additions and 134 deletions

View File

@ -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')

View File

@ -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 (
<Spring
to={{
@ -67,7 +67,7 @@ export default (compose(
connect(
(state, props) => ({
key: props.language,
validInputEntered: validInputEnteredSelector(state),
userInput: !noUserInputSelector(state),
conversationStarted: state.conversationStarted,
quickLinks: state.simulation?.config["questions à l'affiche"]
}),

View File

@ -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,

View File

@ -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 (

View File

@ -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 (
<div id="targetSelection">
<QuickLinks />
<Controls controls={analysis.controls} />
<Controls
controls={chain(({ contrôles }) => contrôles, analysis.cache)}
/>
<div style={{ height: '10px' }}>
<Progress percent={progress} />
</div>
@ -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
}}
/>
</div>
@ -190,15 +187,7 @@ let CurrencyField = withColours(props => {
})
let TargetInputOrValue = withLanguage(
({
target,
targets,
activeInput,
setActiveInput,
language,
noUserInput,
blockingInputControls
}) => (
({ target, targets, activeInput, setActiveInput, language, noUserInput }) => (
<span className="targetInputOrValue">
{activeInput === target.dottedName ? (
<Field
@ -213,8 +202,7 @@ let TargetInputOrValue = withLanguage(
target,
activeInput,
setActiveInput,
noUserInput,
blockingInputControls
noUserInput
}}
/>
)}
@ -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(
<div
className={classNames({
editable: target.question,
attractClick:
target.question && (noUserInput || blockingInputControls)
attractClick: target.question && noUserInput
})}
tabIndex="0"
onClick={this.showField(value)}

View File

@ -85,12 +85,12 @@ export let treat = (rules, rule) => 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

View File

@ -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

View File

@ -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

View File

@ -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) /

View File

@ -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:

View File

@ -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(