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
parent
c36462bd41
commit
f9580f15b5
|
@ -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')
|
||||
|
|
|
@ -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"]
|
||||
}),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) /
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue