From ac8f09fd16406f1365d33c8eded62ee0a9eaaf0c Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Wed, 6 May 2020 22:08:49 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Int=C3=A9gre=20les=20missi?= =?UTF-8?q?ngVariables=20dans=20le=20moteur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/mon-entreprise/simulateurs.js | 5 +- .../components/utils/useNextQuestion.tsx | 16 ++++-- mon-entreprise/source/locales/rules-en.yaml | 3 + mon-entreprise/source/rules/salarié.yaml | 2 + .../AideDéclarationIndépendant/index.tsx | 2 +- mon-entreprise/test/conversation.test.js | 10 +--- publicodes/source/evaluateRule.ts | 57 +++++++++++-------- publicodes/source/index.ts | 50 ++++------------ publicodes/source/mecanisms.tsx | 55 +++++++++--------- publicodes/source/mecanisms/barème.ts | 3 +- publicodes/source/parseReference.js | 11 +++- publicodes/test/mecanisms.test.js | 3 +- .../mécanismes/question-conditionelle.yaml | 2 +- 13 files changed, 107 insertions(+), 112 deletions(-) diff --git a/mon-entreprise/cypress/integration/mon-entreprise/simulateurs.js b/mon-entreprise/cypress/integration/mon-entreprise/simulateurs.js index 3a856d3dc..3ff196db6 100644 --- a/mon-entreprise/cypress/integration/mon-entreprise/simulateurs.js +++ b/mon-entreprise/cypress/integration/mon-entreprise/simulateurs.js @@ -81,10 +81,9 @@ describe('Simulateurs', function() { .type('{selectall}50000') cy.contains('Passer').click() cy.contains('Passer').click() - cy.contains('Début 2020').click() - cy.wait(200) - cy.contains('Suivant').click() cy.contains('ACRE') + cy.contains('Passer').click() + cy.contains('Début 2020').click() }) it('should not have negative value', () => { cy.contains('€/mois').click() diff --git a/mon-entreprise/source/components/utils/useNextQuestion.tsx b/mon-entreprise/source/components/utils/useNextQuestion.tsx index bebbff660..5713b5af2 100644 --- a/mon-entreprise/source/components/utils/useNextQuestion.tsx +++ b/mon-entreprise/source/components/utils/useNextQuestion.tsx @@ -50,7 +50,15 @@ export function getNextSteps( ) const innerKeys = flatten(map(keys, missingVariables)), - missingByTargetsAdvanced = countBy(identity, innerKeys) + missingByTargetsAdvanced = Object.fromEntries( + Object.entries(countBy(identity, innerKeys)).map( + // Give higher score to top level questions + ([name, score]) => [ + name, + score + Math.max(0, 4 - name.split('.').length) + ] + ) + ) const missingByCompound = mergeWith( pair, @@ -116,9 +124,9 @@ export const useNextQuestions = function(): Array { const currentQuestion = useSelector(currentQuestionSelector) const questionsConfig = useSelector(configSelector).questions ?? {} const situation = useSelector(situationSelector) - const missingVariables = useEvaluation(objectifs, { - useDefaultValues: false - }).map(node => node.missingVariables ?? {}) + const missingVariables = useEvaluation(objectifs).map( + node => node.missingVariables ?? {} + ) const nextQuestions = useMemo(() => { return getNextQuestions( missingVariables, diff --git a/mon-entreprise/source/locales/rules-en.yaml b/mon-entreprise/source/locales/rules-en.yaml index 73815631e..5fe6d094d 100644 --- a/mon-entreprise/source/locales/rules-en.yaml +++ b/mon-entreprise/source/locales/rules-en.yaml @@ -2363,6 +2363,9 @@ contrat salarié . plafond sécurité sociale . renonciation proratisation: vieillesse. titre.en: "[automatic] proration waiver" titre.fr: renonciation proratisation +contrat salarié . plafond sécurité sociale . renonciation proratisation . plafond sécurité sociale: + titre.en: "[automatic] social security ceiling" + titre.fr: plafond sécurité sociale contrat salarié . prime d'impatriation: description.en: The impatriation bonus is a part of the remuneration exempt from income tax. description.fr: La prime d'impatriation est une partie de la rémunération diff --git a/mon-entreprise/source/rules/salarié.yaml b/mon-entreprise/source/rules/salarié.yaml index 425e3aa84..090d26b5b 100644 --- a/mon-entreprise/source/rules/salarié.yaml +++ b/mon-entreprise/source/rules/salarié.yaml @@ -1500,6 +1500,8 @@ contrat salarié . plafond sécurité sociale . renonciation proratisation: du plafond de la sécurité sociale (applicable pour les salariés à temps partiel), notamment afin d'augmenter le montant des cotisations vieillesse. par défaut: non + +contrat salarié . plafond sécurité sociale . renonciation proratisation . plafond sécurité sociale: applicable si: temps de travail . quotité de travail < 100% remplace: - règle: plafond sécurité sociale diff --git a/mon-entreprise/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/index.tsx b/mon-entreprise/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/index.tsx index a1ec47deb..13b2ad9c3 100644 --- a/mon-entreprise/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/index.tsx +++ b/mon-entreprise/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/index.tsx @@ -341,7 +341,7 @@ type SimpleFieldProps = { } function SimpleField({ dottedName, question, summary }: SimpleFieldProps) { const dispatch = useDispatch() - const evaluatedRule = useEvaluation(dottedName, { useDefaultValues: false }) + const evaluatedRule = useEvaluation(dottedName) const rules = useContext(EngineContext).getParsedRules() const value = useSelector(situationSelector)[dottedName] const [currentValue, setCurrentValue] = useState(value) diff --git a/mon-entreprise/test/conversation.test.js b/mon-entreprise/test/conversation.test.js index 7377325a4..60eb63dd5 100644 --- a/mon-entreprise/test/conversation.test.js +++ b/mon-entreprise/test/conversation.test.js @@ -61,12 +61,6 @@ describe('conversation', function() { expect( getNextQuestions([engine.evaluate('net').missingVariables])[0] - ).to.equal(undefined) - - expect( - getNextQuestions([ - engine.evaluate('net', { useDefaultValues: false }).missingVariables - ])[0] ).to.equal('cadre') }) @@ -78,9 +72,7 @@ describe('conversation', function() { 'contrat salarié . CDD': 'oui', 'contrat salarié . rémunération . brut de base': '2300' }) - .evaluate('contrat salarié . rémunération . net', { - useDefaultValues: false - }).missingVariables + .evaluate('contrat salarié . rémunération . net').missingVariables ) expect(result).to.include('contrat salarié . CDD . motif') diff --git a/publicodes/source/evaluateRule.ts b/publicodes/source/evaluateRule.ts index 27f09c253..ebbc984fa 100644 --- a/publicodes/source/evaluateRule.ts +++ b/publicodes/source/evaluateRule.ts @@ -23,33 +23,42 @@ export const evaluateApplicability = ( } = evaluatedAttributes, parentDependencies = node.parentDependencies.map(parent => evaluateNode(cache, situation, parsedRules, parent) - ), - isApplicable = - parentDependencies.some(parent => parent?.nodeValue === false) || - notApplicable?.nodeValue === true || - applicable?.nodeValue === false || - disabled?.nodeValue === true - ? false - : [notApplicable, applicable, ...parentDependencies].some( - n => n?.nodeValue === null - ) - ? null - : !notApplicable?.nodeValue && - (applicable?.nodeValue == undefined || !!applicable?.nodeValue), - missingVariables = - isApplicable === false - ? {} - : mergeAll([ - ...parentDependencies.map(parent => parent.missingVariables), - notApplicable?.missingVariables || {}, - disabled?.missingVariables || {}, - applicable?.missingVariables || {} - ]) + ) + + const anyDisabledParent = parentDependencies.find( + parent => parent?.nodeValue === false + ) + + const { nodeValue, missingVariables } = anyDisabledParent + ? anyDisabledParent + : notApplicable?.nodeValue === true + ? { + nodeValue: false, + missingVariables: notApplicable.missingVariables + } + : applicable?.nodeValue === false + ? { nodeValue: false, missingVariables: applicable.missingVariables } + : disabled?.nodeValue === true + ? { nodeValue: false, missingVariables: disabled.missingVariables } + : { + nodeValue: [notApplicable, applicable, ...parentDependencies].some( + n => n?.nodeValue === null + ) + ? null + : !notApplicable?.nodeValue && + (applicable?.nodeValue == undefined || !!applicable?.nodeValue), + missingVariables: mergeAll([ + ...parentDependencies.map(parent => parent.missingVariables), + notApplicable?.missingVariables || {}, + disabled?.missingVariables || {}, + applicable?.missingVariables || {} + ]) + } return { ...node, - isApplicable, - nodeValue: isApplicable, + nodeValue, + isApplicable: nodeValue, missingVariables, parentDependencies, ...evaluatedAttributes diff --git a/publicodes/source/index.ts b/publicodes/source/index.ts index 64d217556..5a2fd06de 100644 --- a/publicodes/source/index.ts +++ b/publicodes/source/index.ts @@ -31,7 +31,6 @@ type EvaluatedSituation = Partial< export type EvaluationOptions = Partial<{ unit: string - useDefaultValues: boolean }> export * from './components' @@ -42,50 +41,28 @@ export { parseRules } export default class Engine { parsedRules: ParsedRules - defaultValues: Situation situation: Situation = {} - cache: Cache - warnings: Array = [] - cacheWithoutDefault: Cache + private cache: Cache + private warnings: Array = [] constructor(rules: string | Rules | ParsedRules) { this.cache = emptyCache() - this.cacheWithoutDefault = emptyCache() - this.parsedRules = typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName ? parseRules(rules) : (rules as ParsedRules) - - this.defaultValues = mapObjIndexed( - (value, name) => - typeof value === 'string' - ? this.evaluateExpression(value, `[valeur par défaut] ${name}`, false) - : value, - collectDefaults(this.parsedRules) - ) as EvaluatedSituation } private resetCache() { this.cache = emptyCache() - this.cacheWithoutDefault = emptyCache() - } - - private situationWithDefaultValues(useDefaultValues = true) { - return { - ...(useDefaultValues ? this.defaultValues : {}), - ...this.situation - } } private evaluateExpression( expression: string, - context: string, - useDefaultValues = true + context: string ): EvaluatedNode { // EN ATTENDANT d'AVOIR une meilleure gestion d'erreur, on va mocker // console.warn - const warnings: string[] = [] const originalWarn = console.warn console.warn = (warning: string) => { this.warnings.push(warning) @@ -93,8 +70,8 @@ export default class Engine { } const result = simplifyNodeUnit( evaluateNode( - useDefaultValues ? this.cache : this.cacheWithoutDefault, - this.situationWithDefaultValues(useDefaultValues), + this.cache, + this.situation, this.parsedRules, parse( this.parsedRules, @@ -121,7 +98,7 @@ export default class Engine { this.situation = mapObjIndexed( (value, name) => typeof value === 'string' - ? this.evaluateExpression(value, `[situation] ${name}`, true) + ? this.evaluateExpression(value, `[situation] ${name}`) : value, situation ) as EvaluatedSituation @@ -136,12 +113,12 @@ export default class Engine { evaluate(expression: string, options?: EvaluationOptions) { let result = this.evaluateExpression( expression, - `[evaluation] ${expression}`, - options?.useDefaultValues ?? true + `[evaluation] ${expression}` ) if (result.category === 'reference' && result.explanation) { result = { nodeValue: result.nodeValue, + missingVariables: result.missingVariables, ...('unit' in result && { unit: result.unit }), ...('temporalValue' in result && { temporalValue: result.temporalValue @@ -164,22 +141,15 @@ export default class Engine { } return result } + controls() { - return evaluateControls( - this.cache, - this.situationWithDefaultValues(), - this.parsedRules - ) + return evaluateControls(this.cache, this.situation, this.parsedRules) } getWarnings() { return this.warnings } - getRules() { - return this.warnings - } - inversionFail(): boolean { return !!this.cache._meta.inversionFail } diff --git a/publicodes/source/mecanisms.tsx b/publicodes/source/mecanisms.tsx index 6b42196c6..f56d3192d 100644 --- a/publicodes/source/mecanisms.tsx +++ b/publicodes/source/mecanisms.tsx @@ -58,21 +58,23 @@ export const mecanismOneOf = (recurse, k, v) => { const evaluate = (cache, situation, parsedRules, node) => { const evaluateOne = child => - evaluateNode(cache, situation, parsedRules, child), - explanation = map(evaluateOne, node.explanation), - values = pluck('nodeValue', explanation), - nodeValue = any(equals(true), values) - ? true - : any(equals(null), values) - ? null - : false, - // Unlike most other array merges of missing variables this is a "flat" merge - // because "one of these conditions" tend to be several tests of the same variable - // (e.g. contract type is one of x, y, z) - missingVariables = - nodeValue == null - ? reduce(mergeWith(max), {}, map(collectNodeMissing, explanation)) - : {} + evaluateNode(cache, situation, parsedRules, child) + const explanation = map(evaluateOne, node.explanation) + + const anyTrue = explanation.find(e => e.nodeValue === true) + const anyNull = explanation.find(e => e.nodeValue === null) + const { nodeValue, missingVariables } = anyTrue ?? + anyNull ?? { + nodeValue: false, + // Unlike most other array merges of missing variables this is a "flat" merge + // because "one of these conditions" tend to be several tests of the same variable + // (e.g. contract type is one of x, y, z) + missingVariables: reduce( + mergeWith(max), + {}, + map(collectNodeMissing, explanation) + ) + } return { ...node, nodeValue, explanation, missingVariables } } @@ -105,14 +107,13 @@ export const mecanismAllOf = (recurse, k, v) => { const evaluate = (cache, situation, parsedRules, node) => { const evaluateOne = child => evaluateNode(cache, situation, parsedRules, child), - explanation = map(evaluateOne, node.explanation), - values = pluck('nodeValue', explanation), - nodeValue = any(equals(false), values) - ? false // court-circuit - : any(equals(null), values) - ? null - : true, - missingVariables = nodeValue == null ? mergeAllMissing(explanation) : {} + explanation = map(evaluateOne, node.explanation) + + const anyFalse = explanation.find(e => e.nodeValue === false) // court-circuit + const { nodeValue, missingVariables } = anyFalse ?? { + nodeValue: explanation.some(e => e.nodeValue === null) ? null : true, + missingVariables: mergeAllMissing(explanation) + } return { ...node, nodeValue, explanation, missingVariables } } @@ -588,10 +589,12 @@ export const mecanismSynchronisation = (recurse, k, v) => { ? path(valuePath, APIExplanation.explanation.defaultValue) : nodeValue - const missingVariables = - APIExplanation.nodeValue === null + const missingVariables = { + ...APIExplanation.missingVariables, + ...(APIExplanation.nodeValue === null ? { [APIExplanation.dottedName]: 1 } - : {} + : {}) + } const explanation = { ...v, API: APIExplanation } return { ...node, nodeValue: safeNodeValue, explanation, missingVariables } } diff --git a/publicodes/source/mecanisms/barème.ts b/publicodes/source/mecanisms/barème.ts index 26b5e1b4f..a4aa3e646 100644 --- a/publicodes/source/mecanisms/barème.ts +++ b/publicodes/source/mecanisms/barème.ts @@ -76,7 +76,8 @@ function evaluateBarème(tranches, assiette, evaluate, cache) { nodeValue: (Math.min(assiette.nodeValue, tranche.plafondValue) - tranche.plancherValue) * - convertUnit(taux.unit, parseUnit(''), taux.nodeValue as number) + convertUnit(taux.unit, parseUnit(''), taux.nodeValue as number), + missingVariables: mergeAllMissing([taux, tranche]) } }) } diff --git a/publicodes/source/parseReference.js b/publicodes/source/parseReference.js index 15f151fa3..d91c9e21b 100644 --- a/publicodes/source/parseReference.js +++ b/publicodes/source/parseReference.js @@ -122,6 +122,7 @@ let evaluateReference = (filter, contextRuleName) => ( if (applicableReplacements.length) { if (applicableReplacements.length > 1) { + // eslint-disable-next-line no-console console.warn(` Règle ${rule.dottedName}: plusieurs remplacements valides ont été trouvés : \n\t${applicableReplacements.map(node => node.rawNode).join('\n\t')} @@ -195,6 +196,14 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v ) } + if (rule.defaultValue != null) { + const evaluation = evaluateNode(cache, situation, rules, rule.defaultValue) + return cacheNode(evaluation.nodeValue ?? evaluation, { + ...evaluation.missingVariables, + [dottedName]: 1 + }) + } + if (rule.formule != null) { const evaluation = evaluateNode(cache, situation, rules, rule) return cacheNode( @@ -205,7 +214,7 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v ) } - return cacheNode(null, { [dottedName]: rule.defaultValue ? 1 : 2 }) + return cacheNode(null, { [dottedName]: 2 }) } export let parseReference = ( diff --git a/publicodes/test/mecanisms.test.js b/publicodes/test/mecanisms.test.js index bb897e30e..2fbb1fbf3 100644 --- a/publicodes/test/mecanisms.test.js +++ b/publicodes/test/mecanisms.test.js @@ -39,8 +39,7 @@ testSuites.forEach(([suiteName, suite]) => { const result = engine .setSituation(situation ?? {}) .evaluate(name, { - unit: defaultUnit, - useDefaultValues: false + unit: defaultUnit }) if (typeof valeur === 'number') { expect(result.nodeValue).to.be.closeTo(valeur, 0.001) diff --git a/publicodes/test/mécanismes/question-conditionelle.yaml b/publicodes/test/mécanismes/question-conditionelle.yaml index 6dc0596ec..39099c279 100644 --- a/publicodes/test/mécanismes/question-conditionelle.yaml +++ b/publicodes/test/mécanismes/question-conditionelle.yaml @@ -13,7 +13,7 @@ famille nombreuse: situation: enfants: oui variables manquantes: ['nombre enfants'] - valeur attendue: null + valeur attendue: true - nom: question non posée situation: enfants: non