Merge pull request #1025 from betagouv/missings
Intégre les missingVariables dans le moteurpull/1032/head
commit
75cbb0c297
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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é.
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue