Merge pull request #1025 from betagouv/missings

Intégre les missingVariables dans le moteur
pull/1032/head
Johan Girod 2020-06-04 13:20:39 +02:00 committed by GitHub
commit 75cbb0c297
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 175 additions and 195 deletions

View File

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

View File

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

View File

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

View File

@ -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<React.ComponentProps<Link>, 'to'>
) {
@ -18,7 +17,6 @@ export default function RuleLink(
<EngineRuleLink
{...props}
engine={engine}
useDefaultValues={props.useDefaultValues ?? true}
documentationPath={sitePaths.documentation.index}
/>
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<SimulatorMetaData>
}
@ -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 }) => (
<Link
className="ui__ interactive card box"
key={sitePath}
@ -117,8 +132,10 @@ export default function Simulateurs() {
<p className="ui__ notice" css="flex: 1">
{description}
</p>
{label && <span className="ui__ label">{label}</span>}
</Link>
))}
)
)}
</div>
</section>
<section>

View File

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

View File

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

View File

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

View File

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

View File

@ -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](#<rulelink-/>) (ci-dessous)
> qui s'en occupe pour nous.
#### <RuleLink />
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 `<Documentation />`
- `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é.

View File

@ -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<Name extends string> = Omit<
React.ComponentProps<Link>,
@ -17,7 +13,6 @@ type RuleLinkProps<Name extends string> = Omit<
engine: Engine<Name>
documentationPath: string
displayIcon?: boolean
useDefaultValues?: boolean
children?: React.ReactNode
}
@ -26,7 +21,6 @@ export function RuleLink<Name extends string>({
engine,
documentationPath,
displayIcon = false,
useDefaultValues = false,
children,
...props
}: RuleLinkProps<Name>) {
@ -34,7 +28,7 @@ export function RuleLink<Name extends string>({
const newPath = documentationPath + '/' + encodeRuleName(dottedName)
return (
<Link to={{ pathname: newPath, state: { useDefaultValues } }} {...props}>
<Link to={newPath} {...props}>
{children || rule.title}{' '}
{displayIcon && rule.icons && <span>{emoji(rule.icons)} </span>}
</Link>
@ -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 (
<RuleLink
engine={engine}
documentationPath={documentationPath}
useDefaultValues={useDefaultValues}
{...props}
/>
)

View File

@ -1,6 +1,5 @@
import { createContext } from 'react'
import Engine from '..'
export const UseDefaultValuesContext = createContext<boolean>(true)
export const BasepathContext = createContext<string>('/documentation')
export const EngineContext = createContext<Engine<string> | null>(null)

View File

@ -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<Names extends string>({
i18n.changeLanguage(language)
}
}, [language])
const state: { useDefaultValues?: boolean } = useLocation().state ?? {}
const useDefaultValues =
('useDefaultValues' in state && state.useDefaultValues) || false
return (
<EngineContext.Provider value={engine}>
<BasepathContext.Provider value={documentationPath}>
<UseDefaultValuesContext.Provider value={useDefaultValues}>
<Route
path={documentationPath + '/:name+'}
render={({ match }) => {
return (
<RulePage
dottedName={decodeRuleName(match.params.name)}
engine={engine}
useDefaultValues={useDefaultValues}
language={'fr'}
/>
)
}}
/>
</UseDefaultValuesContext.Provider>
<Route
path={documentationPath + '/:name+'}
render={({ match }) => {
return (
<RulePage
dottedName={decodeRuleName(match.params.name)}
engine={engine}
language={'fr'}
/>
)
}}
/>
</BasepathContext.Provider>
</EngineContext.Provider>
)

View File

@ -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 <p>Cette règle est introuvable dans la base</p>
}
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 })}
<br />

View File

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

View File

@ -31,7 +31,6 @@ type EvaluatedSituation<Names extends string> = Partial<
export type EvaluationOptions = Partial<{
unit: string
useDefaultValues: boolean
}>
export * from './components'
@ -42,50 +41,28 @@ export { parseRules }
export default class Engine<Names extends string> {
parsedRules: ParsedRules<Names>
defaultValues: Situation<Names>
situation: Situation<Names> = {}
cache: Cache
warnings: Array<string> = []
cacheWithoutDefault: Cache
private cache: Cache
private warnings: Array<string> = []
constructor(rules: string | Rules<Names> | ParsedRules<Names>) {
this.cache = emptyCache()
this.cacheWithoutDefault = emptyCache()
this.parsedRules =
typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName
? parseRules(rules)
: (rules as ParsedRules<Names>)
this.defaultValues = mapObjIndexed(
(value, name) =>
typeof value === 'string'
? this.evaluateExpression(value, `[valeur par défaut] ${name}`, false)
: value,
collectDefaults(this.parsedRules)
) as EvaluatedSituation<Names>
}
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<Names> {
// 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<Names extends string> {
}
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<Names extends string> {
this.situation = mapObjIndexed(
(value, name) =>
typeof value === 'string'
? this.evaluateExpression(value, `[situation] ${name}`, true)
? this.evaluateExpression(value, `[situation] ${name}`)
: value,
situation
) as EvaluatedSituation<Names>
@ -136,12 +113,12 @@ export default class Engine<Names extends string> {
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<Names extends string> {
}
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
}

View File

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

View File

@ -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])
}
})
}

View File

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

View File

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

View File

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