295 lines
8.4 KiB
JavaScript
295 lines
8.4 KiB
JavaScript
// Reference to a variable
|
|
import parseRule from 'Engine/parseRule'
|
|
import React from 'react'
|
|
import { evaluateApplicability } from './evaluateRule'
|
|
import { evaluateNode, mergeMissing } from './evaluation'
|
|
import { getSituationValue } from './getSituationValue'
|
|
import { Leaf } from './mecanismViews/common'
|
|
import { disambiguateRuleReference, findRuleByDottedName } from './rules'
|
|
const getApplicableReplacements = (
|
|
filter,
|
|
contextRuleName,
|
|
cache,
|
|
situation,
|
|
rules,
|
|
rule
|
|
) => {
|
|
const missingVariableList = []
|
|
const applicableReplacements = rule.replacedBy
|
|
.sort(
|
|
(replacement1, replacement2) =>
|
|
!!replacement2.whiteListedNames - !!replacement1.whiteListedNames
|
|
)
|
|
// Remove remplacement not in whitelist
|
|
.filter(
|
|
({ whiteListedNames }) =>
|
|
!whiteListedNames ||
|
|
whiteListedNames.some(name => contextRuleName.startsWith(name))
|
|
)
|
|
.filter(
|
|
({ blackListedNames }) =>
|
|
!blackListedNames ||
|
|
blackListedNames.every(name => !contextRuleName.startsWith(name))
|
|
)
|
|
.filter(({ referenceNode }) => contextRuleName !== referenceNode.dottedName)
|
|
// Remove remplacement defined in a not applicable node
|
|
.filter(({ referenceNode }) => {
|
|
const referenceRule = rules[referenceNode.dottedName]
|
|
const {
|
|
nodeValue: isApplicable,
|
|
missingVariables
|
|
} = evaluateApplicability(cache, situation, rules, referenceRule)
|
|
missingVariableList.push(missingVariables)
|
|
return isApplicable
|
|
})
|
|
// Remove remplacement defined in a node whose situation value is false
|
|
.filter(({ referenceNode }) => {
|
|
const referenceRule = rules[referenceNode.dottedName]
|
|
const situationValue = getSituationValue(
|
|
situation,
|
|
referenceRule.dottedName,
|
|
referenceRule
|
|
)
|
|
if (referenceNode.question && situationValue == null) {
|
|
missingVariableList[referenceNode.dottedName] = 1
|
|
}
|
|
return situationValue !== false
|
|
})
|
|
// Remove remplacement defined in a boolean node whose evaluated value is false
|
|
.filter(({ referenceNode }) => {
|
|
const referenceRule = rules[referenceNode.dottedName]
|
|
if (referenceRule.formule?.explanation?.operationType !== 'comparison') {
|
|
return true
|
|
}
|
|
const { nodeValue: isApplicable, missingVariables } = evaluateNode(
|
|
cache,
|
|
situation,
|
|
rules,
|
|
referenceRule
|
|
)
|
|
missingVariableList.push(missingVariables)
|
|
return isApplicable
|
|
})
|
|
.map(({ referenceNode, replacementNode }) =>
|
|
replacementNode != null
|
|
? evaluateNode(cache, situation, rules, replacementNode)
|
|
: evaluateReference(filter)(cache, situation, rules, referenceNode)
|
|
)
|
|
return [applicableReplacements, missingVariableList]
|
|
}
|
|
|
|
let evaluateReference = (filter, contextRuleName) => (
|
|
cache,
|
|
situation,
|
|
rules,
|
|
node
|
|
) => {
|
|
let rule = rules[node.dottedName]
|
|
|
|
// When a rule exists in different version (created using the `replace` mecanism), we add
|
|
// a redirection in the evaluation of references to use a potential active replacement
|
|
const [
|
|
applicableReplacements,
|
|
missingVariableList
|
|
] = getApplicableReplacements(
|
|
filter,
|
|
contextRuleName,
|
|
cache,
|
|
situation,
|
|
rules,
|
|
rule
|
|
)
|
|
|
|
if (applicableReplacements.length) {
|
|
return applicableReplacements[0]
|
|
}
|
|
|
|
let dottedName = node.dottedName,
|
|
// On va vérifier dans le cache courant, dict, si la variable n'a pas été déjà évaluée
|
|
// En effet, l'évaluation dans le cas d'une variable qui a une formule, est coûteuse !
|
|
cacheName = dottedName + (filter ? '.' + filter : ''),
|
|
cached = cache[cacheName]
|
|
|
|
if (cached) return cached
|
|
let cacheNode = (nodeValue, missingVariables, explanation) => {
|
|
missingVariables = missingVariableList.reduce(
|
|
mergeMissing,
|
|
missingVariables
|
|
)
|
|
cache[cacheName] = {
|
|
...node,
|
|
nodeValue,
|
|
...(explanation && { explanation }),
|
|
missingVariables
|
|
}
|
|
return cache[cacheName]
|
|
}
|
|
const {
|
|
nodeValue: isApplicable,
|
|
missingVariables: condMissingVariables
|
|
} = evaluateApplicability(cache, situation, rules, rule)
|
|
if (!isApplicable) {
|
|
return cacheNode(isApplicable, condMissingVariables)
|
|
}
|
|
const situationValue = getSituationValue(situation, dottedName, rule)
|
|
if (situationValue !== undefined) {
|
|
return cacheNode(situationValue, condMissingVariables, {
|
|
...rule,
|
|
nodeValue: situationValue
|
|
})
|
|
}
|
|
|
|
if (rule.formule != null) {
|
|
const evaluation = evaluateNode(cache, situation, rules, rule)
|
|
return cacheNode(
|
|
evaluation.nodeValue,
|
|
evaluation.missingVariables,
|
|
evaluation
|
|
)
|
|
}
|
|
|
|
return cacheNode(null, { [dottedName]: rule.defaultValue ? 1 : 2 })
|
|
}
|
|
|
|
export let parseReference = (
|
|
rules,
|
|
rule,
|
|
parsedRules,
|
|
filter
|
|
) => partialReference => {
|
|
let dottedName = disambiguateRuleReference(rules, rule, partialReference)
|
|
|
|
let inInversionFormula = rule.formule?.['inversion numérique']
|
|
|
|
let parsedRule =
|
|
parsedRules[dottedName] ||
|
|
// the 'inversion numérique' formula should not exist. The instructions to the evaluation should be enough to infer that an inversion is necessary (assuming it is possible, the client decides this)
|
|
(!inInversionFormula &&
|
|
parseRule(rules, findRuleByDottedName(rules, dottedName), parsedRules))
|
|
|
|
return {
|
|
evaluate: evaluateReference(filter, rule.dottedName),
|
|
//eslint-disable-next-line react/display-name
|
|
jsx: nodeValue => (
|
|
<>
|
|
<Leaf
|
|
classes="variable filtered"
|
|
filter={filter}
|
|
name={partialReference}
|
|
dottedName={dottedName}
|
|
nodeValue={nodeValue}
|
|
unit={parsedRule.unit}
|
|
/>
|
|
</>
|
|
),
|
|
|
|
name: partialReference,
|
|
category: 'reference',
|
|
partialReference,
|
|
dottedName,
|
|
unit: parsedRule.unit
|
|
}
|
|
}
|
|
|
|
// This function is a wrapper that can apply :
|
|
// - temporal transformations to the value of the variable.
|
|
// See the période.yaml test suite for details
|
|
// - filters on the variable to select one part of the variable's 'composantes'
|
|
|
|
const evaluateTransforms = (originalEval, rule, parseResult) => (
|
|
cache,
|
|
situation,
|
|
parsedRules,
|
|
node
|
|
) => {
|
|
// Filter transformation
|
|
let filteringSituation = name =>
|
|
name == 'sys.filter' ? parseResult.filter : situation(name)
|
|
let filteredNode = originalEval(
|
|
cache,
|
|
parseResult.filter ? filteringSituation : situation,
|
|
parsedRules,
|
|
node
|
|
)
|
|
if (!filteredNode.explanation) {
|
|
return filteredNode
|
|
}
|
|
|
|
let nodeValue = filteredNode.nodeValue
|
|
|
|
// Temporal transformation
|
|
let supportedPeriods = ['mois', 'année', 'flexible']
|
|
if (nodeValue == null) return filteredNode
|
|
let ruleToTransform = parsedRules[filteredNode.explanation.dottedName]
|
|
|
|
let inlinePeriodTransform = { mensuel: 'mois', annuel: 'année' }[
|
|
parseResult.temporalTransform
|
|
]
|
|
|
|
// Exceptions
|
|
if (!rule.période && !inlinePeriodTransform && rule.formule) {
|
|
if (supportedPeriods.includes(ruleToTransform?.période))
|
|
throw new Error(
|
|
`Attention, une variable sans période, ${rule.dottedName}, qui appelle une variable à période, ${ruleToTransform.dottedName}, c'est suspect !
|
|
|
|
Si la période de la variable appelée est neutralisée dans la formule de calcul, par exemple un montant mensuel divisé par 30 (comprendre 30 jours), utilisez "période: aucune" pour taire cette erreur et rassurer tout le monde.
|
|
`
|
|
)
|
|
|
|
return filteredNode
|
|
}
|
|
if (!ruleToTransform?.période) return filteredNode
|
|
let environmentPeriod = situation('période') || 'mois'
|
|
let callingPeriod =
|
|
inlinePeriodTransform ||
|
|
(rule.période === 'flexible' ? environmentPeriod : rule.période)
|
|
let calledPeriod =
|
|
ruleToTransform.période === 'flexible'
|
|
? environmentPeriod
|
|
: ruleToTransform.période
|
|
|
|
let transformedNodeValue =
|
|
callingPeriod === 'mois' && calledPeriod === 'année'
|
|
? nodeValue / 12
|
|
: callingPeriod === 'année' && calledPeriod === 'mois'
|
|
? nodeValue * 12
|
|
: nodeValue,
|
|
periodTransform = nodeValue !== transformedNodeValue
|
|
|
|
let result = {
|
|
...filteredNode,
|
|
periodTransform,
|
|
...(periodTransform ? { originPeriodValue: nodeValue } : {}),
|
|
nodeValue: transformedNodeValue,
|
|
explanation: filteredNode.explanation,
|
|
missingVariables: filteredNode.missingVariables
|
|
}
|
|
|
|
return result
|
|
}
|
|
export let parseReferenceTransforms = (
|
|
rules,
|
|
rule,
|
|
parsedRules
|
|
) => parseResult => {
|
|
const referenceName = parseResult.variable.fragments.join(' . ')
|
|
let node = parseReference(rules, rule, parsedRules, parseResult.filter)(
|
|
referenceName
|
|
)
|
|
|
|
return {
|
|
...node,
|
|
// Decorate node with the composante filter (either who is paying, either tax free)
|
|
...(parseResult.filter
|
|
? {
|
|
cotisation: {
|
|
...node.cotisation,
|
|
'dû par': parseResult.filter,
|
|
'impôt sur le revenu': parseResult.filter
|
|
}
|
|
}
|
|
: {}),
|
|
evaluate: evaluateTransforms(node.evaluate, rule, parseResult)
|
|
}
|
|
}
|