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/scripts/i18n/parser.config.js b/mon-entreprise/scripts/i18n/parser.config.js index 43890e090..a17870071 100644 --- a/mon-entreprise/scripts/i18n/parser.config.js +++ b/mon-entreprise/scripts/i18n/parser.config.js @@ -50,7 +50,7 @@ module.exports = { // Namespace separator used in your translation keys // If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance. - output: '../../source/locales/static-analysis-$LOCALE.json', + output: 'source/locales/static-analysis-$LOCALE.json', // Supports $LOCALE and $NAMESPACE injection // Supports JSON (.json) and YAML (.yml) file formats // Where to write the locale files relative to process.cwd() diff --git a/mon-entreprise/scripts/i18n/utils.js b/mon-entreprise/scripts/i18n/utils.js index bc9f083a4..3408b603a 100644 --- a/mon-entreprise/scripts/i18n/utils.js +++ b/mon-entreprise/scripts/i18n/utils.js @@ -117,7 +117,7 @@ function getRulesMissingTranslations() { const getUiMissingTranslations = () => { const staticKeys = require(path.resolve( - '../../source/locales/static-analysis-fr.json' + 'source/locales/static-analysis-fr.json' )) const translatedKeys = parse(fs.readFileSync(UiTranslationPath, 'utf-8')) diff --git a/mon-entreprise/source/components/RuleLink.tsx b/mon-entreprise/source/components/RuleLink.tsx index 4eff1933f..88d297017 100644 --- a/mon-entreprise/source/components/RuleLink.tsx +++ b/mon-entreprise/source/components/RuleLink.tsx @@ -1,4 +1,4 @@ -import Engine, { RuleLink as EngineRuleLink } from 'publicodes' +import { RuleLink as EngineRuleLink } from 'publicodes' import React, { useContext } from 'react' import { Link } from 'react-router-dom' import { DottedName } from 'Rules' @@ -8,7 +8,6 @@ import { SitePathsContext } from './utils/SitePathsContext' export default function RuleLink( props: { dottedName: DottedName - useDefaultValues?: boolean displayIcon?: boolean } & Omit, 'to'> ) { @@ -18,7 +17,6 @@ export default function RuleLink( ) 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/locales/ui-en.yaml b/mon-entreprise/source/locales/ui-en.yaml index 227667a5d..552e05b86 100644 --- a/mon-entreprise/source/locales/ui-en.yaml +++ b/mon-entreprise/source/locales/ui-en.yaml @@ -6,6 +6,7 @@ A quoi servent mes cotisations ?: What's included in my contributions? Accueil: Home Afficher la description publicode: Display publicode description +Aide à la déclaration de revenu: Income tax return assistance Aide à la déclaration de revenus au titre de l'année 2019: Help with your 2019 income tax return Alors: Then Année d'activité: Years of activity @@ -23,6 +24,7 @@ Changer: Change Chercher dans la documentation: Search the documentation Choisir la forme juridique: Choose your legal status Choisir plus tard: Choose later +Chômage partiel: Partial unemployment Code d'intégration: Integration Code Commencer: Get started "Commerçant, artisan, ou libéral ?": Trader, craftsman, or liberal? @@ -31,6 +33,7 @@ Continuer: Continue Coronavirus: Coronavirus Cotisations: Contributions Cotisations sociales: Social contributions +Covid 19: Covid 19 "Covid-19 : Découvrez les mesures de soutien aux entreprises": "Covid-19: Find out about business support measures" "Covid-19 : Découvrir les mesures de soutien aux entreprises": "Covid-19: Discovering Business Support Measures" Coût pour l'entreprise: Cost to the company @@ -1007,11 +1010,15 @@ simulateurs: faible: Low accuracy moyenne: Medium accuracy résumé: + aide-déclaration-revenu-indep: Easily calculate the amount of payroll taxes to + report on your 2019 income tax return. artiste-auteur: Estimating the social security contributions of an artist or author assimilé: | Calculate the income of an officer of a minority SAS, SASU or SARL auto: | Calculate the income (or turnover) of an auto-entrepreneur + chômage-partiel: Simulate the net income paid to the employee, as well as the + total remaining cost to the company if the partial activity is used. comparaison: > Simulate the differences between the plans (contributions, retirement, maternity, illness, etc.) diff --git a/mon-entreprise/source/rules/déclaration-revenu-indépendant.yaml b/mon-entreprise/source/rules/déclaration-revenu-indépendant.yaml index 07b138645..f513f64fd 100644 --- a/mon-entreprise/source/rules/déclaration-revenu-indépendant.yaml +++ b/mon-entreprise/source/rules/déclaration-revenu-indépendant.yaml @@ -9,7 +9,6 @@ aide déclaration revenu indépendant 2019: aide déclaration revenu indépendant 2019 . nature de l'activité: remplace: entreprise . catégorie d'activité question: Quelle est la nature de votre activité ? - par défaut: "'commerciale ou industrielle'" formule: une possibilité: choix obligatoire: oui 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/source/sites/mon-entreprise.fr/pages/Simulateurs/Home.tsx b/mon-entreprise/source/sites/mon-entreprise.fr/pages/Simulateurs/Home.tsx index 62c4c4124..71c9a4b88 100644 --- a/mon-entreprise/source/sites/mon-entreprise.fr/pages/Simulateurs/Home.tsx +++ b/mon-entreprise/source/sites/mon-entreprise.fr/pages/Simulateurs/Home.tsx @@ -14,6 +14,7 @@ export function useSimulatorsMetadata() { icône: string description?: string sitePath: string + label?: string } return [ @@ -67,14 +68,29 @@ export function useSimulatorsMetadata() { icône: '📊', description: t( 'simulateurs.résumé.comparaison', - 'Simulez les différences entre les régimes (cotisations,retraite, maternité, maladie, etc.)' + 'Découvrir les différences entre les régimes (cotisations,retraite, maternité, maladie, etc.)' ), sitePath: sitePaths.simulateurs.comparaison }, { - name: t('Coronavirus'), - icône: '👨‍🔬', + name: t('Chômage partiel'), + description: t( + 'simulateurs.résumé.chômage-partiel', + "Simuler le revenu net versé au salarié, ainsi que le coût total restant à charge pour l'entreprise en cas de recours à l'activité partielle." + ), + icône: '😷', + label: t('Covid 19'), sitePath: sitePaths.coronavirus + }, + { + name: t('Aide à la déclaration de revenu'), + description: t( + 'simulateurs.résumé.aide-déclaration-revenu-indep', + 'Calculer facilement les montants des charges sociales à reporter dans votre déclaration de revenu 2019.' + ), + icône: '✍️', + label: t('Indépendant'), + sitePath: sitePaths.gérer.déclarationIndépendant } ] as Array } @@ -101,9 +117,8 @@ export default function Simulateurs() { // dernière ligne. style={{ maxWidth: 1100, margin: 'auto' }} > - {simulatorsMetadata - .filter(({ name }) => name !== 'Coronavirus') - .map(({ name, description, sitePath, icône }) => ( + {simulatorsMetadata.map( + ({ name, description, sitePath, icône, label }) => ( {description}

+ {label && {label}} - ))} + ) + )}
diff --git a/mon-entreprise/source/sites/publi.codes/Studio.tsx b/mon-entreprise/source/sites/publi.codes/Studio.tsx index a236600b0..d0d637820 100644 --- a/mon-entreprise/source/sites/publi.codes/Studio.tsx +++ b/mon-entreprise/source/sites/publi.codes/Studio.tsx @@ -59,7 +59,6 @@ export default function Studio() { useEffect(() => { history.replace({ pathname, - state: { useDefaultValues: true }, search: `?code=${encodeURIComponent(debouncedEditorValue)}` }) }, [debouncedEditorValue, history]) @@ -122,8 +121,7 @@ export const Results = ({ onClickShare, rules }: ResultsProps) => { target => history.replace({ pathname: ruleToPaths[target], - search, - state: { useDefaultValues: true } + search }), [ruleToPaths, history, search] ) 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/mon-entreprise/test/regressions/aide-déclaration-indépendants.yaml b/mon-entreprise/test/regressions/aide-déclaration-indépendants.yaml index 49c465824..873ab1b6b 100644 --- a/mon-entreprise/test/regressions/aide-déclaration-indépendants.yaml +++ b/mon-entreprise/test/regressions/aide-déclaration-indépendants.yaml @@ -62,16 +62,16 @@ IJSS (indemnité sécurité sociale): dirigeant . rémunération totale: 50000 €/an ACRE: - - entreprise . ACRE: oui + - aide déclaration revenu indépendant 2019 . ACRE: oui dirigeant . rémunération totale: 50000 €/an - - entreprise . ACRE: oui + - aide déclaration revenu indépendant 2019 . ACRE: oui dirigeant . rémunération totale: 15000 €/an - - entreprise . ACRE: oui + - aide déclaration revenu indépendant 2019 . ACRE: oui dirigeant . rémunération totale: 5000 €/an - - entreprise . ACRE: oui + - aide déclaration revenu indépendant 2019 . ACRE: oui entreprise . date de création: 01/07/2018 dirigeant . rémunération totale: 10000 €/an - - entreprise . ACRE: oui + - aide déclaration revenu indépendant 2019 . ACRE: oui entreprise . date de création: 01/07/2019 dirigeant . rémunération totale: 10000 €/an diff --git a/mon-entreprise/test/regressions/simulations.jest.js b/mon-entreprise/test/regressions/simulations.jest.js index 99a59f46e..14b690fea 100644 --- a/mon-entreprise/test/regressions/simulations.jest.js +++ b/mon-entreprise/test/regressions/simulations.jest.js @@ -107,6 +107,10 @@ it('calculate aide-déclaration-indépendant', () => { runSimulations( aideDéclarationIndépendantsSituations, aideDéclarationConfig.objectifs, - aideDéclarationConfig.situation + { + "aide déclaration revenu indépendant 2019 . nature de l'activité": + "'commerciale ou industrielle'", + ...aideDéclarationConfig.situation + } ) }) diff --git a/publicodes/docs/api.md b/publicodes/docs/api.md index 837b32371..9b5ca4127 100644 --- a/publicodes/docs/api.md +++ b/publicodes/docs/api.md @@ -243,8 +243,6 @@ dans un cache. Par conséquent, les prochains appels seront plus rapides. - `unit`: spécifie l'unité dans laquelle le résultat doit être retourné. Si la valeur retournée par le calcul est un nombre, ce dernier sera converti dans l'unité demandée. Ainsi `evaluate('prix', {unit: '€'})` équivaut à `evaluate('prix [€]')`. Une erreur est levée si l'unité n'est pas compatible avec la formule. - - `useDefaultValues` (par défaut `true`): option pour forcer l'utilisation des valeurs par défaut des règles. - Si sa valeur est à `false` et qu'il manque des valeurs dans la situation pour que le calcul soit effectué, ces dernières seront remontée dans les `missingsVariables` de l'objet retourné, et la valeur sera `null`. **Retourne** Un objet javascript de type `EvaluatedNode` contenant la valeur calculée. @@ -255,8 +253,7 @@ Un objet javascript de type `EvaluatedNode` contenant la valeur calculée. > Utilisez la fonction `formatNode(evaluationResult)` autant que possible pour > afficher la valeur retournée. -- `missingVariables`: contient les valeur manquante lorsque `useDefaultValues` - est mis à `false`. +- `missingVariables`: contient les règles dont la valeur est manquante dans la situation - `nodeValue`: la valeur calculée - `isApplicable`: si l'expression évaluée est une référence à une règle, alors ce booléen indique si la règle est applicable ou non @@ -307,14 +304,6 @@ action (il est affiché sur l'écran de droite). - `language`: le language dans lequel afficher la documentation (pour l'instant, seul `fr` et `en` sont supportés) -> Note : les valeurs des règles `par défaut` ne sont pas utilisée dans la doc. -> Si l'on souhaite afficher la documentation avec les calculs utilisant les -> valeurs par défaut, il suffit d'ajouter la clé `useDefaultValues: true` dans -> le `state` de l'objet -> [`location`](https://reacttraining.com/react-router/web/api/location) du -> navigateur. On peut également utiliser [RuleLink](#) (ci-dessous) -> qui s'en occupe pour nous. - #### Composant react permettant de faire un lien vers une page de la documentation. @@ -327,5 +316,4 @@ Par défaut, le texte affiché est le nom de la règle. montée. Doit correspondre à celui précisé pour le composant `` - `dottedName`: le nom de la règle à afficher - `displayIcon`: affiche l'icône de la règle dans le lien (par défaut à `false`) -- `useDefaultValues`: utilise les valeurs `par défaut` des règles (par défaut à `false`) - `children`: N'importe quel noeud react. Par défaut, c'est le nom de la règle qui est utilisé. diff --git a/publicodes/source/components/RuleLink.tsx b/publicodes/source/components/RuleLink.tsx index 92dd03d3a..d943a214b 100644 --- a/publicodes/source/components/RuleLink.tsx +++ b/publicodes/source/components/RuleLink.tsx @@ -3,11 +3,7 @@ import emoji from 'react-easy-emoji' import { Link } from 'react-router-dom' import Engine from '..' import { encodeRuleName } from '../ruleUtils' -import { - BasepathContext, - EngineContext, - UseDefaultValuesContext -} from './contexts' +import { BasepathContext, EngineContext } from './contexts' type RuleLinkProps = Omit< React.ComponentProps, @@ -17,7 +13,6 @@ type RuleLinkProps = Omit< engine: Engine documentationPath: string displayIcon?: boolean - useDefaultValues?: boolean children?: React.ReactNode } @@ -26,7 +21,6 @@ export function RuleLink({ engine, documentationPath, displayIcon = false, - useDefaultValues = false, children, ...props }: RuleLinkProps) { @@ -34,7 +28,7 @@ export function RuleLink({ const newPath = documentationPath + '/' + encodeRuleName(dottedName) return ( - + {children || rule.title}{' '} {displayIcon && rule.icons && {emoji(rule.icons)} } @@ -49,13 +43,11 @@ export function RuleLinkWithContext( throw new Error('an engine should be provided in context') } const documentationPath = useContext(BasepathContext) - const useDefaultValues = useContext(UseDefaultValuesContext) return ( ) diff --git a/publicodes/source/components/contexts.tsx b/publicodes/source/components/contexts.tsx index 606a1355d..b7a92e592 100644 --- a/publicodes/source/components/contexts.tsx +++ b/publicodes/source/components/contexts.tsx @@ -1,6 +1,5 @@ import { createContext } from 'react' import Engine from '..' -export const UseDefaultValuesContext = createContext(true) export const BasepathContext = createContext('/documentation') export const EngineContext = createContext | null>(null) diff --git a/publicodes/source/components/index.tsx b/publicodes/source/components/index.tsx index 0dce0e271..12bb2e2b2 100644 --- a/publicodes/source/components/index.tsx +++ b/publicodes/source/components/index.tsx @@ -1,13 +1,9 @@ import React, { useEffect } from 'react' -import { Route, useLocation } from 'react-router-dom' +import { Route } from 'react-router-dom' import Engine from '..' import i18n from '../i18n' import { decodeRuleName, encodeRuleName } from '../ruleUtils' -import { - BasepathContext, - EngineContext, - UseDefaultValuesContext -} from './contexts' +import { BasepathContext, EngineContext } from './contexts' import RulePage from './rule/Rule' export { RuleLink } from './RuleLink' @@ -28,27 +24,21 @@ export function Documentation({ i18n.changeLanguage(language) } }, [language]) - const state: { useDefaultValues?: boolean } = useLocation().state ?? {} - const useDefaultValues = - ('useDefaultValues' in state && state.useDefaultValues) || false return ( - - { - return ( - - ) - }} - /> - + { + return ( + + ) + }} + /> ) diff --git a/publicodes/source/components/rule/Rule.tsx b/publicodes/source/components/rule/Rule.tsx index 6e02e8476..3cba6d4d6 100644 --- a/publicodes/source/components/rule/Rule.tsx +++ b/publicodes/source/components/rule/Rule.tsx @@ -10,21 +10,12 @@ import RuleHeader from './Header' import References from './References' import RuleSource from './RuleSource' -// let LazySource = React.lazy(() => import('../../../../mon-entreprise/source/components/RuleSource')) - -export default function Rule({ - dottedName, - useDefaultValues, - engine, - language -}) { - const [viewSource, setViewSource] = useState(false) +export default function Rule({ dottedName, engine, language }) { if (!engine.getParsedRules()[dottedName]) { return

Cette règle est introuvable dans la base

} - const rule = engine.evaluate(dottedName, { - useDefaultValues - }) + const rule = engine.evaluate(dottedName) + const isSetInStituation = engine.situation[dottedName] !== undefined const { description, question } = rule return ( @@ -43,7 +34,9 @@ export default function Rule({ padding: '1rem' }} > - {rule.nodeValue != null && ( + {((rule.defaultValue?.nodeValue == null && + rule.nodeValue != null) || + (rule.defaultValue?.nodeValue != null && isSetInStituation)) && ( <> {formatValue(rule, { language })}
diff --git a/publicodes/source/evaluateRule.ts b/publicodes/source/evaluateRule.ts index 27f09c253..7fe1e7d60 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