🔥 change l'api du moteur

pull/949/head
Johan Girod 2020-03-26 16:03:19 +01:00
parent e43cb1df32
commit bf078b2938
71 changed files with 1090 additions and 1944 deletions

View File

@ -1230,7 +1230,6 @@ contrat salarié . rémunération . primes . activité:
contrat salarié . rémunération . primes . activité . base:
titre: primes d'activité
unité: €/mois
question: Quel est le montant des primes liées à l'activité du salarié ?
par défaut: 0
@ -3136,6 +3135,7 @@ contrat salarié . taxe sur les salaires . barème:
contrat salarié . profession spécifique:
question: Le salarié exerce t-il l'une des professions suivantes ?
par défaut: non
formule:
une possibilité:
possibilités:

View File

@ -1,12 +1,11 @@
import { ThemeColorsContext } from 'Components/utils/colors'
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
import Value from 'Components/Value'
import { findRuleByDottedName } from 'Engine/rules'
import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { useSelector } from 'react-redux'
import { animated, config, useSpring } from 'react-spring'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
import répartitionSelector from 'Selectors/repartitionSelectors'
import { Rule } from 'Types/rule'
import { isIE } from '../utils'
@ -53,12 +52,12 @@ export function DistributionBranch({
icon,
distribution
}: DistributionBranchProps) {
const rules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
const [intersectionRef, brancheInViewport] = useDisplayOnIntersecting({
threshold: 0.5
})
const { color } = useContext(ThemeColorsContext)
const branche = findRuleByDottedName(rules, dottedName)
const branche = rules[dottedName]
const montant = brancheInViewport ? value : 0
const styles = useSpring({
config: ANIMATION_SPRING,

View File

@ -1,6 +1,6 @@
import { ThemeColorsContext } from 'Components/utils/colors'
import Value from 'Components/Value'
import { findRuleByDottedName, getRuleFromAnalysis } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import React, { Fragment, useContext } from 'react'
import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux'
@ -64,7 +64,7 @@ export default function PaySlip() {
<Trans>Part salarié</Trans>
</h4>
{cotisations.map(([brancheDottedName, cotisationList]) => {
let branche = findRuleByDottedName(parsedRules, brancheDottedName)
let branche = parsedRules[brancheDottedName]
return (
<Fragment key={branche.dottedName}>
<h5 className="payslip__cotisationTitle">

View File

@ -92,15 +92,15 @@ export let SalaireNetSection = ({ getRule }) => {
<Trans>Salaire net</Trans>
</h4>
{netImposable && <Line rule={netImposable} />}
{(avantagesEnNature.nodeValue || retenueTitresRestaurant.nodeValue) && (
{(avantagesEnNature?.nodeValue || retenueTitresRestaurant?.nodeValue) && (
<Line
rule={getRule('contrat salarié . rémunération . net de cotisations')}
/>
)}
{!!avantagesEnNature.nodeValue && (
{!!avantagesEnNature?.nodeValue && (
<Line negative rule={avantagesEnNature} />
)}
{!!retenueTitresRestaurant.nodeValue && (
{!!retenueTitresRestaurant?.nodeValue && (
<Line negative rule={retenueTitresRestaurant} />
)}

View File

@ -1,6 +1,6 @@
import { ThemeColorsContext } from 'Components/utils/colors'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { nameLeaf } from 'Engine/rules'
import { nameLeaf } from 'Engine/ruleUtils'
import React, { useContext } from 'react'
import { Link } from 'react-router-dom'
import { Rule } from 'Types/rule'

View File

@ -1,13 +1,13 @@
import { goBackToSimulation } from 'Actions/actions'
import { ScrollToTop } from 'Components/utils/Scroll'
import { decodeRuleName, findRuleByDottedName } from 'Engine/rules.js'
import { decodeRuleName } from 'Engine/ruleUtils.js'
import React from 'react'
import { Trans } from 'react-i18next'
import { connect, useSelector } from 'react-redux'
import { Redirect } from 'react-router-dom'
import {
flatRulesSelector,
noUserInputSelector,
parsedRulesSelector,
situationBranchNameSelector
} from 'Selectors/analyseSelectors'
import { DottedName } from 'Types/rule'
@ -16,7 +16,7 @@ import './RulePage.css'
import SearchButton from './SearchButton'
export default function RulePage({ match }) {
const flatRules = useSelector(flatRulesSelector)
const parsedRules = useSelector(parsedRulesSelector)
const brancheName = useSelector(situationBranchNameSelector)
const valuesToShow = !useSelector(noUserInputSelector)
let name = match?.params?.name,
@ -36,8 +36,7 @@ export default function RulePage({ match }) {
)
}
if (!findRuleByDottedName(flatRules, decodedRuleName))
return <Redirect to="/404" />
if (!parsedRules[decodedRuleName]) return <Redirect to="/404" />
return renderRule(decodedRuleName as DottedName)
}

View File

@ -2,7 +2,7 @@ import Distribution from 'Components/Distribution'
import PaySlip from 'Components/PaySlip'
import StackedBarChart from 'Components/StackedBarChart'
import { ThemeColorsContext } from 'Components/utils/colors'
import { getRuleFromAnalysis } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import React, { useContext, useRef } from 'react'
import emoji from 'react-easy-emoji'
import { Trans, useTranslation } from 'react-i18next'

View File

@ -10,7 +10,7 @@ import PeriodSwitch from 'Components/PeriodSwitch'
import ComparaisonConfig from 'Components/simulationConfigs/rémunération-dirigeant.yaml'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
import { getRuleFromAnalysis } from 'Engine/rules.js'
import { getRuleFromAnalysis } from 'Engine/ruleUtils.js'
import revenusSVG from 'Images/revenus.svg'
import { default as React, useCallback, useContext, useState } from 'react'
import emoji from 'react-easy-emoji'

View File

@ -1,11 +1,11 @@
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { parentName } from 'Engine/rules.js'
import { parentName } from 'Engine/ruleUtils.js'
import { pick, sortBy, take } from 'ramda'
import React, { useContext, useEffect, useState } from 'react'
import FuzzyHighlighter, { Highlighter } from 'react-fuzzy-highlighter'
import { useTranslation } from 'react-i18next'
import { Link, Redirect, useHistory } from 'react-router-dom'
import { Rule } from 'Types/rule'
import { DottedName, Rule } from 'Types/rule'
import Worker from 'worker-loader!./SearchBar.worker.js'
import { capitalise0 } from '../utils'
import './SearchBar.css'
@ -13,7 +13,7 @@ import './SearchBar.css'
const worker = new Worker()
type SearchBarProps = {
rules: Array<Rule>
rules: { [name in DottedName]: Rule }
showDefaultList: boolean
finally?: () => void
}
@ -60,7 +60,7 @@ export default function SearchBar({
useEffect(() => {
worker.postMessage({
rules: rules.map(
rules: Object.values(rules).map(
pick(['title', 'espace', 'description', 'name', 'dottedName'])
)
})
@ -256,7 +256,9 @@ export default function SearchBar({
i18n.t('noresults', {
defaultValue: "Nous n'avons rien trouvé…"
})}
{showDefaultList && !input ? renderOptions(rules) : renderOptions()}
{showDefaultList && !input
? renderOptions(Object.values(rules))
: renderOptions()}
</>
)
}

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'
import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
import Overlay from './Overlay'
import SearchBar from './SearchBar'
@ -11,7 +11,7 @@ type SearchButtonProps = {
}
export default function SearchButton({ invisibleButton }: SearchButtonProps) {
const flatRules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
const [visible, setVisible] = useState(false)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@ -34,7 +34,7 @@ export default function SearchButton({ invisibleButton }: SearchButtonProps) {
<h1>
<Trans>Chercher dans la documentation</Trans>
</h1>
<SearchBar showDefaultList={false} finally={close} rules={flatRules} />
<SearchBar showDefaultList={false} finally={close} rules={rules} />
</Overlay>
) : invisibleButton ? null : (
<button

View File

@ -1,24 +1,23 @@
import { explainVariable } from 'Actions/actions'
import Overlay from 'Components/Overlay'
import { Markdown } from 'Components/utils/markdown'
import { findRuleByDottedName } from 'Engine/rules'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
import References from '../rule/References'
import './Aide.css'
export default function Aide() {
const explained = useSelector((state: RootState) => state.explainedVariable)
const flatRules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
const dispatch = useDispatch()
const stopExplaining = () => dispatch(explainVariable())
if (!explained) return null
let rule = findRuleByDottedName(flatRules, explained),
let rule = rules[explained],
text = rule.description,
refs = rule.références

View File

@ -1,7 +1,7 @@
import { goToQuestion, resetSimulation } from 'Actions/actions'
import Overlay from 'Components/Overlay'
import Value from 'Components/Value'
import { getRuleFromAnalysis } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import React from 'react'
import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'

View File

@ -1,7 +1,6 @@
import { goToQuestion, validateStepWithValue } from 'Actions/actions'
import QuickLinks from 'Components/QuickLinks'
import RuleInput from 'Engine/RuleInput'
import { findRuleByDottedName } from 'Engine/rules'
import React from 'react'
import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'
@ -9,8 +8,8 @@ import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
currentQuestionSelector,
flatRulesSelector,
nextStepsSelector
nextStepsSelector,
parsedRulesSelector
} from 'Selectors/analyseSelectors'
import * as Animate from 'Ui/animate'
import Aide from './Aide'
@ -23,7 +22,7 @@ export type ConversationProps = {
export default function Conversation({ customEndMessages }: ConversationProps) {
const dispatch = useDispatch()
const flatRules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
const currentQuestion = useSelector(currentQuestionSelector)
const previousAnswers = useSelector(
(state: RootState) => state.simulation?.foldedSteps || []
@ -34,7 +33,7 @@ export default function Conversation({ customEndMessages }: ConversationProps) {
dispatch(
validateStepWithValue(
currentQuestion,
findRuleByDottedName(flatRules, currentQuestion).defaultValue
rules[currentQuestion].defaultValue
)
)
const goToPrevious = () =>
@ -46,7 +45,7 @@ export default function Conversation({ customEndMessages }: ConversationProps) {
}
const DecoratedInputComponent = FormDecorator(RuleInput)
return flatRules && nextSteps.length ? (
return rules && nextSteps.length ? (
<>
<Aide />
<div tabIndex={0} style={{ outline: 'none' }} onKeyDown={handleKeyDown}>

View File

@ -1,10 +1,9 @@
import { explainVariable } from 'Actions/actions'
import { findRuleByDottedName } from 'Engine/rules'
import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
import { DottedName } from 'Types/rule'
import { TrackerContext } from '../utils/withTracker'
import './Explicable.css'
@ -13,12 +12,12 @@ export default function Explicable({ dottedName }: { dottedName: DottedName }) {
const tracker = useContext(TrackerContext)
const dispatch = useDispatch()
const explained = useSelector((state: RootState) => state.explainedVariable)
const flatRules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
// Rien à expliquer ici, ce n'est pas une règle
if (dottedName == null) return null
let rule = findRuleByDottedName(flatRules, dottedName)
let rule = rules[dottedName]
if (rule.description == null) return null

View File

@ -1,11 +1,10 @@
import { updateSituation } from 'Actions/actions'
import Explicable from 'Components/conversation/Explicable'
import { findRuleByDottedName } from 'Engine/rules'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import {
flatRulesSelector,
parsedRulesSelector,
situationSelector
} from 'Selectors/analyseSelectors'
@ -21,7 +20,7 @@ export default function FormDecorator(RenderField) {
return function FormStep({ dottedName }) {
const dispatch = useDispatch()
const situation = useSelector(situationSelector)
const flatRules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
const language = useTranslation().i18n.language
const submit = source =>
@ -38,8 +37,7 @@ export default function FormDecorator(RenderField) {
return (
<div className="step">
<h3>
{findRuleByDottedName(flatRules, dottedName).question}{' '}
<Explicable dottedName={dottedName} />
{rules[dottedName].question} <Explicable dottedName={dottedName} />
</h3>
<fieldset>
@ -48,7 +46,7 @@ export default function FormDecorator(RenderField) {
value={situation[dottedName]}
onChange={setFormValue}
onSubmit={submit}
rules={flatRules}
rules={rules}
/>
</fieldset>
</div>

View File

@ -1,5 +1,4 @@
import { SitePathsContext } from 'Components/utils/withSitePaths'
import { findRuleByDottedName } from 'Engine/rules'
import React, { useContext } from 'react'
import emoji from 'react-easy-emoji'
import { Link } from 'react-router-dom'
@ -22,7 +21,7 @@ export default function Namespace({ dottedName, flatRules, color }) {
)
.map(fragments => {
let ruleName = fragments.join(' . '),
rule = findRuleByDottedName(flatRules, ruleName)
rule = flatRules[ruleName]
if (!rule) {
throw new Error(
`Attention, il se peut que la règle ${ruleName}, ait été définie avec un namespace qui n'existe pas.`

View File

@ -2,8 +2,7 @@ import { ThemeColorsContext } from 'Components/utils/colors'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
import mecanisms from 'Engine/mecanisms.yaml'
import { findRuleByDottedName, findRuleByNamespace } from 'Engine/rules'
import { isEmpty } from 'ramda'
import { filter, isEmpty } from 'ramda'
import React, { Suspense, useContext, useState } from 'react'
import emoji from 'react-easy-emoji'
import { Helmet } from 'react-helmet'
@ -12,8 +11,8 @@ import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import {
exampleAnalysisSelector,
flatRulesSelector,
noUserInputSelector,
parsedRulesSelector,
ruleAnalysisSelector
} from 'Selectors/analyseSelectors'
import Animate from 'Ui/animate'
@ -30,7 +29,7 @@ let LazySource = React.lazy(() => import('./RuleSource'))
export default AttachDictionary(mecanisms)(function Rule({ dottedName }) {
const currentExample = useSelector(state => state.currentExample)
const flatRules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
const valuesToShow = !useSelector(noUserInputSelector)
const analysedRule = useSelector(state =>
ruleAnalysisSelector(state, { dottedName })
@ -42,9 +41,15 @@ export default AttachDictionary(mecanisms)(function Rule({ dottedName }) {
const [viewSource, setViewSource] = useState(false)
const { t } = useTranslation()
let flatRule = findRuleByDottedName(flatRules, dottedName)
let { type, name, acronyme, title, description, question, icon } = flatRule,
namespaceRules = findRuleByNamespace(flatRules, dottedName)
let rule = rules[dottedName]
let { type, name, acronyme, title, description, question, icon } = rule,
namespaceRules = filter(
rule =>
rule.dottedName.startsWith(dottedName) &&
rule.dottedName.split(' . ').length ===
dottedName.split(' . ').length + 1,
rules
)
let displayedRule = analysedExample || analysedRule
const renderToggleSourceButton = () => {
return (
@ -99,8 +104,8 @@ export default AttachDictionary(mecanisms)(function Rule({ dottedName }) {
type,
description,
question,
flatRule,
flatRules,
flatRule: rule,
flatRules: rules,
name,
acronyme,
title,
@ -183,10 +188,10 @@ export default AttachDictionary(mecanisms)(function Rule({ dottedName }) {
</ul>
</section>
)}
{flatRule.note && (
{rule.note && (
<section id="notes">
<h3>Note : </h3>
<Markdown source={flatRule.note} />
<Markdown source={rule.note} />
</section>
)}
<Examples
@ -197,7 +202,7 @@ export default AttachDictionary(mecanisms)(function Rule({ dottedName }) {
{!isEmpty(namespaceRules) && (
<NamespaceRulesList {...{ namespaceRules }} />
)}
{renderReferences(flatRule)}
{renderReferences(rule)}
</section>
{renderToggleSourceButton()}
</Animate.fromBottom>
@ -216,7 +221,7 @@ function NamespaceRulesList({ namespaceRules }) {
<Trans>Pages associées</Trans>
</h2>
<ul>
{namespaceRules.map(r => (
{Object.values(namespaceRules).map(r => (
<li key={r.name}>
<Link
style={{

View File

@ -17,7 +17,7 @@ export default function RuleSource({ dottedName }: RuleSourceProps) {
Code source <br />
<code>{dottedName}</code>
</h2>
<PublicodeHighlighter source={safeDump(source)} />
<PublicodeHighlighter source={safeDump({ dottedName: source })} />
</div>
)
}

View File

@ -6,12 +6,10 @@ import SendButton from 'Components/conversation/SendButton'
import CurrencyInput from 'Components/CurrencyInput/CurrencyInput'
import PercentageField from 'Components/PercentageField'
import ToggleSwitch from 'Components/ui/ToggleSwitch'
import { is, prop, unless } from 'ramda'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { DottedName, Rule } from 'Types/rule'
import DateInput from '../components/conversation/DateInput'
import { findRuleByDottedName, queryRule } from './rules'
export const binaryOptionChoices = [
{ value: 'non', label: 'Non' },
@ -20,7 +18,7 @@ export const binaryOptionChoices = [
type Value = string | number | object | boolean
type Props = {
rules: Array<Rule>
rules: { [name in DottedName]: Rule }
dottedName: DottedName
onChange: (value: Value) => void
useSwitch?: boolean
@ -46,7 +44,7 @@ export default function RuleInput({
className,
onSubmit
}: Props) {
let rule = findRuleByDottedName(rules, dottedName)
let rule = rules[dottedName]
let unit = rule.unit || rule.defaultUnit
let language = useTranslation().i18n.language
@ -63,7 +61,6 @@ export default function RuleInput({
defaultValue: rule.defaultValue,
suggestions: rule.suggestions
}
if (getVariant(rule)) {
return (
<Question
@ -125,16 +122,16 @@ export default function RuleInput({
return <Input {...commonProps} unit={unit} />
}
let getVariant = rule => queryRule(rule)('formule . une possibilité')
let getVariant = rule => rule?.formule?.explanation['une possibilité']
export let buildVariantTree = (allRules, path) => {
let rec = path => {
let node = findRuleByDottedName(allRules, path)
let node = allRules[path]
if (!node) throw new Error(`La règle ${path} est introuvable`)
let variant = getVariant(node),
variants = variant && unless(is(Array), prop('possibilités'))(variant),
variants = variant && node.formule.explanation['possibilités'],
shouldBeExpanded = variant && true, //variants.find( v => relevantPaths.find(rp => contains(path + ' . ' + v)(rp) )),
canGiveUp = variant && !variant['choix obligatoire']
canGiveUp = variant && !node.formule.explanation['choix obligatoire']
return Object.assign(
node,

View File

@ -1,22 +1,19 @@
import { evaluateNode } from 'Engine/evaluation'
import { filter, map, path, pipe, unnest, values } from 'ramda'
let getControls = path(['explanation', 'contrôles'])
export let evaluateControls = (cache, situationGate, parsedRules) =>
pipe(
values,
filter(getControls),
map(rule =>
getControls(rule).map(control => ({
...control,
export let evaluateControls = (cache, situationGate, parsedRules) => {
return Object.values(parsedRules)
.filter(rule => !!rule.contrôles)
.map(rule =>
rule.contrôles.map(contrôle => ({
...contrôle,
evaluated: evaluateNode(
{ ...cache, contextRule: [rule.dottedName] },
situationGate,
parsedRules,
control.testExpression
contrôle.testExpression
)
}))
),
unnest,
filter(control => control.evaluated.nodeValue === true)
)(cache)
)
.flat()
.filter(contrôle => contrôle.evaluated.nodeValue === true)
}

View File

@ -3,7 +3,6 @@ import {
countBy,
descend,
flatten,
fromPairs,
head,
identity,
keys,
@ -37,9 +36,6 @@ type Explanation = {
dottedName: DottedName
}
export let collectMissingVariablesByTarget = (targets: Explanation[] = []) =>
fromPairs(targets.map(target => [target.dottedName, target.missingVariables]))
export let getNextSteps = missingVariablesByTarget => {
let byCount = ([, [count]]) => count
let byScore = ([, [, score]]) => score
@ -62,6 +58,3 @@ export let getNextSteps = missingVariablesByTarget => {
sortedPairs = sortWith([descend(byCount), descend(byScore) as any], pairs)
return map(head, sortedPairs)
}
export let collectMissingVariables = targets =>
getNextSteps(collectMissingVariablesByTarget(targets))

View File

@ -1,5 +1,5 @@
import { dropLast, isEmpty, last } from 'ramda'
import { joinName, splitName } from './rules'
import { joinName, splitName } from './ruleUtils'
let evaluateBottomUp = situationGate => startingFragments => {
let rec = (parentFragments, childFragments = []) =>

View File

@ -1,46 +1,48 @@
import { safeLoad } from 'js-yaml'
import { evaluateControls } from 'Engine/controls'
import { Simulation } from 'Reducers/rootReducer'
import { DottedName, Rule } from 'Types/rule'
import { Rule } from 'Types/rule'
import { DottedName } from './../types/rule'
import { evaluateNode } from './evaluation'
import { collectDefaults, enrichRule, rulesFr } from './rules'
import { parseAll } from './traverse'
import { parseUnit } from './units'
import parseRules from './parseRules'
import { collectDefaults } from './ruleUtils'
import { parseUnit, Unit } from './units'
const emptyCache = {
_meta: { contextRule: [], defaultUnits: [] }
}
type EngineConfig = {
rules?: string | Array<any> | object
extra?: string | Array<any> | object
rules: string | object
useDefaultValues?: boolean
}
let enrichRules = input => {
const rules =
typeof input === 'string' ? safeLoad(input.replace(/\t/g, ' ')) : input
const rulesList = Array.isArray(rules)
? rules
: Object.entries(rules).map(([dottedName, rule]) => ({
dottedName,
...(rule as any)
}))
return rulesList.map(enrichRule)
type Cache = {
_meta: {
contextRule: Array<string>
defaultUnits: Array<Unit>
inversionFail?: {
given: string
estimated: string
}
}
}
export { parseRules }
export default class Engine {
rules: Array<Rule>
parsedRules: Record<DottedName, Rule>
defaultValues: Simulation['situation']
situation: Simulation['situation'] = {}
cache = { ...emptyCache }
cache: Cache = { ...emptyCache }
constructor(config: EngineConfig = {}) {
this.rules = [
...(config?.rules ? enrichRules(config.rules) : rulesFr),
...(config?.extra ? enrichRules(config.extra) : [])
]
this.parsedRules = parseAll(this.rules) as any
this.defaultValues = collectDefaults(this.rules)
constructor({ rules, useDefaultValues = true }: EngineConfig) {
this.parsedRules =
typeof rules === 'object' &&
!!Object.values(rules).filter(Boolean)[0].dottedName
? rules
: (parseRules(rules) as any)
this.defaultValues = useDefaultValues
? collectDefaults(this.parsedRules)
: {}
}
private resetCache() {
@ -50,12 +52,14 @@ export default class Engine {
setSituation(situation: Simulation['situation'] = {}) {
this.situation = situation
this.resetCache()
return this
}
setDefaultUnits(defaultUnits = []) {
setDefaultUnits(defaultUnits: string[] = []) {
this.cache._meta.defaultUnits = defaultUnits.map(unit =>
parseUnit(unit)
) as any
return this
}
evaluate(expression: string | Array<string>) {
@ -68,19 +72,25 @@ export default class Engine {
this.situationGate,
this.parsedRules,
this.parsedRules[expr]
// TODO: To support expressions (with operations, unit conversion,
// etc.) it should be enough to replace the above line with :
// parse(this.parsedRules, { dottedName: '' }, this.parsedRules)(expr)
// But currently there are small side effects (null values converted
// to 0), so we need to modify a little bit the engine before enabling
// publicode expressions in the UI.
)
: null)
)
: // TODO: To support expressions (with operations, unit conversion,
// etc.) it should be enough to replace the above line with :
// parse(this.parsedRules, { dottedName: '' }, this.parsedRules)(expr)
// But currently there are small side effects (null values converted
// to 0), so we need to modify a little bit the engine before enabling
// publicode expressions in the UI.
null)
)
return Array.isArray(expression) ? results : results[0]
}
controls() {
return evaluateControls(this.cache, this.situationGate, this.parsedRules)
}
// TODO : this should be private
getCache(): Cache {
return this.cache
}
situationGate = (dottedName: string) =>
this.situation[dottedName] || this.defaultValues[dottedName]
this.situation[dottedName] ?? this.defaultValues[dottedName]
}

View File

@ -6,11 +6,11 @@ import React, { useContext } from 'react'
import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
import { DottedName, Rule } from 'Types/rule'
import { LinkButton } from 'Ui/Button'
import { capitalise0 } from '../../utils'
import { encodeRuleName, findRuleByDottedName } from '../rules'
import { encodeRuleName } from '../ruleUtils'
import mecanismColors from './colors'
type NodeValuePointerProps = {
@ -48,7 +48,14 @@ type NodeProps = {
children: React.ReactNode
}
export function Node({ classes, name, value, children, inline, unit }: NodeProps) {
export function Node({
classes,
name,
value,
children,
inline,
unit
}: NodeProps) {
let termDefinition = contains('mecanism', classes) && name
return (
@ -127,8 +134,8 @@ export function Leaf({
unit
}: LeafProps) {
const sitePaths = useContext(SitePathsContext)
const flatRules = useSelector(flatRulesSelector)
let rule = findRuleByDottedName(flatRules, dottedName)
const rules = useSelector(parsedRulesSelector)
let rule = rules[dottedName]
const title = rule.title || capitalise0(name)
return (
<span className={classNames(classes, 'leaf')}>

View File

@ -2,18 +2,40 @@ import { decompose } from 'Engine/mecanisms/utils'
import variations from 'Engine/mecanisms/variations'
import { convertNodeToUnit } from 'Engine/nodeUnits'
import { inferUnit, isPercentUnit } from 'Engine/units'
import { any, equals, evolve, is, map, max, mergeWith, min, path, pluck, reduce, toPairs } from 'ramda'
import {
any,
equals,
evolve,
is,
map,
max,
mergeWith,
min,
path,
pluck,
reduce,
toPairs
} from 'ramda'
import React from 'react'
import 'react-virtualized/styles.css'
import { typeWarning } from './error'
import { collectNodeMissing, defaultNode, evaluateArray, evaluateNode, evaluateObject, makeJsx, mergeAllMissing, parseObject } from './evaluation'
import {
collectNodeMissing,
defaultNode,
evaluateArray,
evaluateNode,
evaluateObject,
makeJsx,
mergeAllMissing,
parseObject
} from './evaluation'
import Allègement from './mecanismViews/Allègement'
import { Node, SimpleRuleLink } from './mecanismViews/common'
import InversionNumérique from './mecanismViews/InversionNumérique'
import Product from './mecanismViews/Product'
import Recalcul from './mecanismViews/Recalcul'
import Somme from './mecanismViews/Somme'
import { disambiguateRuleReference, findRuleByDottedName } from './rules'
import { disambiguateRuleReference } from './ruleUtils'
import uniroot from './uniroot'
import { parseUnit } from './units'
@ -123,16 +145,10 @@ export let findInversion = (situationGate, parsedRules, v, dottedName) => {
le salaire net, a été renseigné ?
*/
let candidates = inversions
.map(i =>
disambiguateRuleReference(
Object.values(parsedRules),
parsedRules[dottedName],
i
)
)
.map(i => disambiguateRuleReference(parsedRules, dottedName, i))
.map(name => {
let userInput = situationGate(name) != undefined
let rule = findRuleByDottedName(parsedRules, name)
let rule = parsedRules[name]
if (!userInput) return null
return {
fixedObjectiveRule: rule,
@ -260,11 +276,7 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => {
let cache = { _meta: { ...currentCache._meta, inRecalcul: true } } // Create an empty cache
let amendedSituation = Object.fromEntries(
Object.keys(node.avec).map(dottedName => [
disambiguateRuleReference(
parsedRules,
{ dottedName: dottedNameContext },
dottedName
),
disambiguateRuleReference(parsedRules, dottedNameContext, dottedName),
node.avec[dottedName]
])
)

View File

@ -7,7 +7,7 @@ import { evaluateNode, mergeMissing } from './evaluation'
import { getSituationValue } from './getSituationValue'
import { Leaf } from './mecanismViews/common'
import { convertNodeToUnit, getNodeDefaultUnit } from './nodeUnits'
import { disambiguateRuleReference, findRuleByDottedName } from './rules'
import { disambiguateRuleReference } from './ruleUtils'
import { areUnitConvertible } from './units'
const getApplicableReplacements = (
filter,
@ -198,15 +198,18 @@ export let parseReference = (
parsedRules,
filter
) => partialReference => {
let dottedName = disambiguateRuleReference(rules, rule, partialReference)
let dottedName = disambiguateRuleReference(
rules,
rule.dottedName,
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))
(!inInversionFormula && parseRule(rules, dottedName, parsedRules))
const unit =
parsedRule.unit || parsedRule.formule?.unit || parsedRule.defaultUnit
return {

View File

@ -3,17 +3,22 @@ import RuleLink from 'Components/RuleLink'
import { evolve, map } from 'ramda'
import React from 'react'
import { Trans } from 'react-i18next'
import { coerceArray } from '../utils'
import { capitalise0, coerceArray } from '../utils'
import { warning } from './error'
import evaluate from './evaluateRule'
import { evaluateNode, makeJsx, mergeAllMissing } from './evaluation'
import { Node } from './mecanismViews/common'
import { parse } from './parse'
import { disambiguateRuleReference, findParentDependencies } from './rules'
import {
disambiguateRuleReference,
findParentDependencies,
nameLeaf
} from './ruleUtils'
import { parseUnit } from './units'
export default (rules, rule, parsedRules) => {
if (parsedRules[rule.dottedName]) return parsedRules[rule.dottedName]
parsedRules[rule.dottedName] = 'being parsed'
export default function parseRule(rules, dottedName, parsedRules) {
if (parsedRules[dottedName]) return parsedRules[dottedName]
parsedRules[dottedName] = 'being parsed'
/*
The parseRule function will traverse the tree of the `rule` and produce an
AST, an object containing other objects containing other objects... Some of
@ -25,7 +30,34 @@ export default (rules, rule, parsedRules) => {
functions are attached to the objects of the AST. They will be evaluated
during the evaluation phase, called "analyse".
*/
let rule = rules[dottedName] || {}
const name = nameLeaf(dottedName)
let unit = rule.unité && parseUnit(rule.unité)
let defaultUnit =
rule['unité par défaut'] && parseUnit(rule['unité par défaut'])
if (defaultUnit && unit) {
warning(
dottedName,
'Le paramètre `unité` est plus contraignant que `unité par défaut`.',
'Si vous souhaitez que la valeur de votre variable soit toujours la même unité, gardez `unité`'
)
}
rule = {
...rule,
dottedName,
name,
type: rule.type,
title: capitalise0(rule['titre'] || name),
defaultValue: rule['par défaut'],
examples: rule['exemples'],
icons: rule['icônes'],
summary: rule['résumé'],
unit,
defaultUnit
}
let parentDependencies = findParentDependencies(rules, rule)
let root = { ...rule, parentDependencies }
@ -36,8 +68,7 @@ export default (rules, rule, parsedRules) => {
// condition d'applicabilité de la règle
parentDependencies: parents =>
parents.map(parent => {
let node = parse(rules, rule, parsedRules)(parent.dottedName)
let node = parse(rules, rule, parsedRules)(parent)
let jsx = (nodeValue, explanation) => (
<ShowValuesConsumer>
{showValues =>
@ -72,7 +103,7 @@ export default (rules, rule, parsedRules) => {
'applicable si': evolveCond('applicable si', rule, rules, parsedRules),
'rend non applicable': nonApplicableRules =>
coerceArray(nonApplicableRules).map(referenceName => {
return disambiguateRuleReference(rules, rule, referenceName)
return disambiguateRuleReference(rules, rule.dottedName, referenceName)
}),
remplace: evolveReplacement(rules, rule, parsedRules),
formule: value => {
@ -111,14 +142,6 @@ export default (rules, rule, parsedRules) => {
},
contrôles: map((control: any) => {
let testExpression = parse(rules, rule, parsedRules)(control.si)
if (
!testExpression.explanation &&
!(testExpression.category === 'reference')
)
throw new Error(
'Ce contrôle ne semble pas être compris :' + control['si']
)
return {
dottedName: rule.dottedName,
level: control['niveau'],
@ -130,10 +153,8 @@ export default (rules, rule, parsedRules) => {
})
})(root)
// On sauvegarde la règle dans les parsedRules
parsedRules[rule.dottedName] = {
// Pas de propriété explanation et jsx ici car on est parti du (mauvais)
// principe que 'non applicable si' et 'formule' sont particuliers, alors
// qu'ils pourraient être rangé avec les autres mécanismes
...parsedRoot,
evaluate,
parsed: true,
@ -241,11 +262,17 @@ let evolveReplacement = (rules, rule, parsedRules) => replacements =>
.map(
names =>
names &&
names.map(name => disambiguateRuleReference(rules, rule, name))
names.map(name =>
disambiguateRuleReference(rules, rule.dottedName, name)
)
)
return {
referenceName: disambiguateRuleReference(rules, rule, referenceName),
referenceName: disambiguateRuleReference(
rules,
rule.dottedName,
referenceName
),
replacementNode,
whiteListedNames,
blackListedNames

View File

@ -0,0 +1,77 @@
import parseRule from 'Engine/parseRule'
import { safeLoad } from 'js-yaml'
import { parseReference } from './parseReference'
/*
Dans ce fichier, les règles YAML sont parsées.
Elles expriment un langage orienté expression, les expressions étant
- préfixes quand elles sont des 'mécanismes' (des mot-clefs représentant des calculs courants dans la loi)
- infixes pour les feuilles : des tests d'égalité, d'inclusion, des comparaisons sur des variables ou tout simplement la variable elle-même, ou une opération effectuée sur la variable
*/
export default function parseRules(rules) {
rules =
typeof rules === 'string' ? safeLoad(rules.replace(/\t/g, ' ')) : rules
/* First we parse each rule one by one. When a mechanism is encountered, it is
recursively parsed. When a reference to a variable is encountered, a
'variable' node is created, we don't parse variables recursively. */
let parsedRules = {}
/* A rule `A` can disable a rule `B` using the rule `rend non applicable: B`
in the definition of `A`. We need to map these exonerations to be able to
retreive them from `B` */
let nonApplicableMapping: Record<string, any> = {}
let replacedByMapping: Record<string, any> = {}
Object.keys(rules).map(dottedName => {
const rule = parseRule(rules, dottedName, parsedRules)
if (rule['rend non applicable']) {
nonApplicableMapping[rule.dottedName] = rule['rend non applicable']
}
const replaceDescriptors = rule['remplace']
if (replaceDescriptors) {
replaceDescriptors.forEach(
descriptor =>
(replacedByMapping[descriptor.referenceName] = [
...(replacedByMapping[descriptor.referenceName] ?? []),
{ ...descriptor, referenceName: rule.dottedName }
])
)
}
})
Object.entries(nonApplicableMapping).forEach(([a, b]) => {
b.forEach(ruleName => {
parsedRules[ruleName].isDisabledBy.push(
parseReference(rules, parsedRules[ruleName], parsedRules)(a)
)
})
})
Object.entries(replacedByMapping).forEach(([a, b]) => {
parsedRules[a].replacedBy = b.map(({ referenceName, ...other }) => ({
referenceNode: parseReference(
rules,
parsedRules[referenceName],
parsedRules
)(referenceName),
...other
}))
})
/* Then we need to infer units. Since only references to variables have been created, we need to wait for the latter map to complete before starting this job. Consider this example :
A = B * C
B = D / E
C unité km
D unité
E unité km
*
* When parsing A's formula, we don't know the unit of B, since only the final nodes have units (it would be too cumbersome to specify a unit to each variable), and B hasn't been parsed yet.
*
* */
return parsedRules
}

View File

@ -1,27 +1,26 @@
import Value from 'Components/Value'
import rules from 'Publicode/rules'
import React, { createContext, useContext, useMemo } from 'react'
import Engine from '.'
export const EngineContext = createContext<{
engine: Engine | null
error: string | null
}>({ engine: new Engine(), error: null })
}>({ engine: new Engine({ rules }), error: null })
type InputProps = {
rules?: any
extra?: any
situation?: any
children: React.ReactNode
}
export function Provider({ rules, extra, situation, children }: InputProps) {
export function Provider({ rules, situation, children }: InputProps) {
const [engine, error] = useMemo(() => {
try {
return [new Engine({ rules, extra }), null]
return [new Engine({ rules }), null]
} catch (err) {
return [null, (err?.message ?? err.toString()) as string]
}
}, [rules, extra])
}, [rules])
if (engine !== null && !Object.is(situation, engine.situation)) {
engine.setSituation(situation)
}

179
source/engine/ruleUtils.js Normal file
View File

@ -0,0 +1,179 @@
import {
assoc,
dropLast,
filter,
isNil,
join,
last,
map,
pipe,
propEq,
range,
reduce,
reject,
split,
take
} from 'ramda'
import { coerceArray } from '../utils'
export const splitName = split(' . ')
export const joinName = join(' . ')
export const parentName = pipe(splitName, dropLast(1), joinName)
export const nameLeaf = pipe(splitName, last)
export const encodeRuleName = name =>
encodeURI(
name
.replace(/\s\.\s/g, '/')
.replace(/-/g, '\u2011') // replace with a insecable tiret to differenciate from space
.replace(/\s/g, '-')
)
export let decodeRuleName = name =>
decodeURI(
name
.replace(/\//g, ' . ')
.replace(/-/g, ' ')
.replace(/\u2011/g, '-')
)
export let ruleParents = dottedName => {
let fragments = splitName(dottedName) // dottedName ex. [CDD . événements . rupture]
return range(1, fragments.length)
.map(nbEl => take(nbEl)(fragments))
.map(joinName) // -> [ [CDD . événements . rupture], [CDD . événements], [CDD
.reverse()
}
export let disambiguateRuleReference = (rules, contextName, partialName) => {
const possibleDottedName = [
contextName,
...ruleParents(contextName),
''
].map(x => (x ? x + ' . ' + partialName : partialName))
const dottedName = possibleDottedName.find(name => name in rules)
if (!dottedName) {
throw new Error(`La référence '${partialName}' est introuvable.
Vérifiez que l'orthographe et l'espace de nom sont corrects`)
}
return dottedName
}
export function collectDefaults(parsedRules) {
return Object.values(parsedRules).reduce(
(acc, rule) => ({
...acc,
...(rule?.defaultValue != null && {
[rule.dottedName]: rule.defaultValue
})
}),
{}
)
}
/*********************************
Autres
*/
/* Traduction */
const translateContrôle = (prop, rule, translation, lang) =>
assoc(
'contrôles',
rule.contrôles.map((control, i) => ({
...control,
message: translation[`${prop}.${i}.${lang}`]?.replace(
/^\[automatic\] /,
''
)
})),
rule
)
const translateSuggestion = (prop, rule, translation, lang) =>
assoc(
'suggestions',
Object.entries(rule.suggestions).reduce(
(acc, [name, value]) => ({
...acc,
[translation[`${prop}.${name}.${lang}`]?.replace(
/^\[automatic\] /,
''
)]: value
}),
{}
),
rule
)
export const attributesToTranslate = [
'titre',
'description',
'question',
'résumé',
'suggestions',
'contrôles',
'note'
]
export let translateAll = (translations, flatRules) => {
let translationsOf = rule => translations[rule.dottedName],
translateProp = (lang, translation) => (rule, prop) => {
if (prop === 'contrôles' && rule?.contrôles) {
return translateContrôle(prop, rule, translation, lang)
}
if (prop === 'suggestions' && rule?.suggestions) {
return translateSuggestion(prop, rule, translation, lang)
}
let propTrans = translation[prop + '.' + lang]
propTrans = propTrans?.replace(/^\[automatic\] /, '')
return propTrans ? assoc(prop, propTrans, rule) : rule
},
translateRule = (lang, translations, props) => rule => {
let ruleTrans = translationsOf(rule)
return ruleTrans
? reduce(translateProp(lang, ruleTrans), rule, props)
: rule
}
return map(
translateRule('en', translations, attributesToTranslate),
flatRules
)
}
export let findParentDependencies = (rules, rule) => {
// A parent dependency means that one of a rule's parents is not just a namespace holder, it is a boolean question. E.g. is it a fixed-term contract, yes / no
// When it is resolved to false, then the whole branch under it is disactivated (non applicable)
// It lets those children omit obvious and repetitive parent applicability tests
let parentDependencies = ruleParents(rule.dottedName)
return pipe(
map(parent => ({ dottedName: parent, ...rules[parent] })),
reject(isNil),
filter(
//Find the first "calculable" parent
({ question, unit, formule }) =>
(question && !unit && !formule) ||
(question && formule?.['une possibilité'] !== undefined) ||
(typeof formule === 'string' && formule.includes(' = ')) ||
formule === 'oui' ||
formule === 'non' ||
formule?.['une de ces conditions'] ||
formule?.['toutes ces conditions']
),
map(parent => parent.dottedName)
)(parentDependencies)
}
export let getRuleFromAnalysis = analysis => dottedName => {
if (!analysis) {
throw new Error("[getRuleFromAnalysis] The analysis can't be nil !")
}
let rule = coerceArray(analysis) // In some simulations, there are multiple "branches" : the analysis is run with e.g. 3 different input situations
.map(
analysis =>
analysis.cache[dottedName]?.explanation || // the cache stores a reference to a variable, the variable is contained in the 'explanation' attribute
analysis.targets.find(propEq('dottedName', dottedName))
)
.filter(Boolean)[0]
if (process.env.NODE_ENV !== 'production' && !rule) {
console.warn(`[getRuleFromAnalysis] Unable to find the rule ${dottedName}`)
}
return rule
}

View File

@ -1,310 +0,0 @@
import { parseUnit } from 'Engine/units'
import rawRules from 'Publicode/rules'
import {
assoc,
chain,
dropLast,
filter,
fromPairs,
is,
isNil,
join,
last,
map,
path,
pipe,
propEq,
props,
range,
reduce,
reduced,
reject,
split,
take,
toPairs,
trim,
when
} from 'ramda'
import translations from '../locales/rules-en.yaml'
// TODO - should be in UI, not engine
import { capitalise0, coerceArray } from '../utils'
import { syntaxError, warning } from './error'
/***********************************
Functions working on one rule */
export let enrichRule = rule => {
try {
const dottedName = rule.dottedName || rule.nom
const name = nameLeaf(dottedName)
let unit = rule.unité && parseUnit(rule.unité)
let defaultUnit =
rule['unité par défaut'] && parseUnit(rule['unité par défaut'])
if (defaultUnit && unit) {
warning(
dottedName,
'Le paramètre `unité` est plus contraignant que `unité par défaut`.',
'Si vous souhaitez que la valeur de votre variable soit toujours la même unité, gardez `unité`'
)
}
return {
...rule,
dottedName,
name,
type: rule.type,
title: capitalise0(rule['titre'] || name),
defaultValue: rule['par défaut'],
examples: rule['exemples'],
icons: rule['icônes'],
summary: rule['résumé'],
unit,
defaultUnit
}
} catch (e) {
syntaxError(
rule.dottedName || rule.nom,
'Problème dans la lecture des champs de la règle',
e
)
}
}
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
export let disambiguateExampleSituation = (rules, rule) =>
pipe(
toPairs,
map(([k, v]) => [disambiguateRuleReference(rules, rule, k), v]),
fromPairs
)
export let hasKnownRuleType = rule => rule && enrichRule(rule).type
export let splitName = split(' . '),
joinName = join(' . ')
export let parentName = pipe(splitName, dropLast(1), joinName)
export let nameLeaf = pipe(splitName, last)
export let encodeRuleName = name =>
encodeURI(
name
.replace(/\s\.\s/g, '/')
.replace(/-/g, '\u2011') // replace with a insecable tiret to differenciate from space
.replace(/\s/g, '-')
)
export let decodeRuleName = name =>
decodeURI(
name
.replace(/\//g, ' . ')
.replace(/-/g, ' ')
.replace(/\u2011/g, '-')
)
export let ruleParents = dottedName => {
let fragments = splitName(dottedName) // dottedName ex. [CDD . événements . rupture]
return range(1, fragments.length)
.map(nbEl => take(nbEl)(fragments))
.reverse() // -> [ [CDD . événements . rupture], [CDD . événements], [CDD] ]
}
/* Les variables peuvent être exprimées dans la formule d'une règle relativement à son propre espace de nom, pour une plus grande lisibilité. Cette fonction résoud cette ambiguité.
*/
export let disambiguateRuleReference = (
allRules,
{ dottedName },
partialName
) => {
let pathPossibilities = [
[], // the top level namespace
...ruleParents(dottedName), // the parents namespace
splitName(dottedName) // the rule's own namespace
],
found = reduce(
(res, path) => {
let dottedNameToCheck = [...path, partialName].join(' . ')
return when(
is(Object),
reduced
)(findRuleByDottedName(allRules, dottedNameToCheck))
},
null,
pathPossibilities
)
if (found?.dottedName) {
return found.dottedName
}
throw new Error(`La référence '${partialName}' est introuvable.
Vérifiez que l'orthographe et l'espace de nom sont corrects`)
}
export let collectDefaults = pipe(
map(props(['dottedName', 'defaultValue'])),
reject(([, v]) => v === undefined),
fromPairs
)
/****************************************
Méthodes de recherche d'une règle */
export let findRuleByName = (allRules, query) =>
(Array.isArray(allRules) ? allRules : Object.values(allRules)).find(
({ name }) => name === query
)
export let findRulesByName = (allRules, query) =>
(Array.isArray(allRules) ? allRules : Object.values(allRules)).filter(
({ name }) => name === query
)
export let findRuleByDottedName = (allRules, dottedName) =>
Array.isArray(allRules)
? allRules.find(rule => rule.dottedName == dottedName)
: allRules[dottedName]
export let findRule = (rules, nameOrDottedName) =>
nameOrDottedName.includes(' . ')
? findRuleByDottedName(rules, nameOrDottedName)
: findRuleByName(rules, nameOrDottedName)
export let findRuleByNamespace = (allRules, ns) =>
allRules.filter(rule => parentName(rule.dottedName) === ns)
/*********************************
Autres */
export let queryRule = rule => query => path(query.split(' . '))(rule)
export let nestedSituationToPathMap = situation => {
if (situation == undefined) return {}
let rec = (o, currentPath) =>
typeof o === 'object'
? chain(([k, v]) => rec(v, [...currentPath, trim(k)]), toPairs(o))
: [[currentPath.join(' . '), o + '']]
return fromPairs(rec(situation, []))
}
/* Traduction */
const translateContrôle = (prop, rule, translation, lang) =>
assoc(
'contrôles',
rule.contrôles.map((control, i) => ({
...control,
message: translation[`${prop}.${i}.${lang}`]?.replace(
/^\[automatic\] /,
''
)
})),
rule
)
const translateSuggestion = (prop, rule, translation, lang) =>
assoc(
'suggestions',
Object.entries(rule.suggestions).reduce(
(acc, [name, value]) => ({
...acc,
[translation[`${prop}.${name}.${lang}`]?.replace(
/^\[automatic\] /,
''
)]: value
}),
{}
),
rule
)
export const attributesToTranslate = [
'titre',
'description',
'question',
'résumé',
'suggestions',
'contrôles',
'note'
]
export let translateAll = (translations, flatRules) => {
let translationsOf = rule => translations[rule.dottedName],
translateProp = (lang, translation) => (rule, prop) => {
if (prop === 'contrôles' && rule?.contrôles) {
return translateContrôle(prop, rule, translation, lang)
}
if (prop === 'suggestions' && rule?.suggestions) {
return translateSuggestion(prop, rule, translation, lang)
}
let propTrans = translation[prop + '.' + lang]
propTrans = propTrans?.replace(/^\[automatic\] /, '')
return propTrans ? assoc(prop, propTrans, rule) : rule
},
translateRule = (lang, translations, props) => rule => {
let ruleTrans = translationsOf(rule)
return ruleTrans
? reduce(translateProp(lang, ruleTrans), rule, props)
: rule
}
return map(
translateRule('en', translations, attributesToTranslate),
flatRules
)
}
const rulesToList = rulesObject =>
Object.entries(rulesObject).map(([dottedName, rule]) => ({
dottedName,
...rule
}))
export const buildFlatRules = rulesObject =>
rulesToList(rulesObject).map(enrichRule)
// On enrichit la base de règles avec des propriétés dérivées de celles du YAML
export let rules = translateAll(translations, rulesToList(rawRules)).map(rule =>
enrichRule(rule)
)
export let rulesFr = buildFlatRules(rawRules)
export let findParentDependencies = (rules, rule) => {
// A parent dependency means that one of a rule's parents is not just a namespace holder, it is a boolean question. E.g. is it a fixed-term contract, yes / no
// When it is resolved to false, then the whole branch under it is disactivated (non applicable)
// It lets those children omit obvious and repetitive parent applicability tests
let parentDependencies = ruleParents(rule.dottedName).map(joinName)
return pipe(
map(parent => findRuleByDottedName(rules, parent)),
reject(isNil),
filter(
//Find the first "calculable" parent
({ question, unit, formule }) =>
(question && !unit && !formule) ||
(question && formule?.['une possibilité'] !== undefined) ||
(typeof formule === 'string' && formule.includes(' = ')) ||
formule === 'oui' ||
formule === 'non' ||
formule?.['une de ces conditions'] ||
formule?.['toutes ces conditions']
)
)(parentDependencies)
}
export let getRuleFromAnalysis = analysis => dottedName => {
if (!analysis) {
throw new Error("[getRuleFromAnalysis] The analysis can't be nil !")
}
let rule = coerceArray(analysis) // In some simulations, there are multiple "branches" : the analysis is run with e.g. 3 different input situations
.map(
analysis =>
analysis.cache[dottedName]?.explanation || // the cache stores a reference to a variable, the variable is contained in the 'explanation' attribute
analysis.targets.find(propEq('dottedName', dottedName))
)
.filter(Boolean)[0]
if (process.env.NODE_ENV !== 'production' && !rule) {
console.warn(`[getRuleFromAnalysis] Unable to find the rule ${dottedName}`)
}
return rule
}

View File

@ -1,168 +0,0 @@
import { evaluateControls } from 'Engine/controls'
import parseRule from 'Engine/parseRule'
import { chain, path } from 'ramda'
import { DottedName, EvaluatedRule } from 'Types/rule'
import { evaluateNode } from './evaluation'
import { parseReference } from './parseReference'
import {
disambiguateRuleReference,
findRule,
findRuleByDottedName
} from './rules'
import { parseUnit, Unit } from './units'
/*
Dans ce fichier, les règles YAML sont parsées.
Elles expriment un langage orienté expression, les expressions étant
- préfixes quand elles sont des 'mécanismes' (des mot-clefs représentant des calculs courants dans la loi)
- infixes pour les feuilles : des tests d'égalité, d'inclusion, des comparaisons sur des variables ou tout simplement la variable elle-même, ou une opération effectuée sur la variable
*/
/*
-> Notre règle est naturellement un AST (car notation préfixe dans le YAML)
-> préliminaire : les expression infixes devront être parsées,
par exemple ainsi : https://github.com/Engelberg/instaparse#transforming-the-tree
-> Notre règle entière est un AST, qu'il faut maintenant traiter :
- faire le calcul (déterminer les valeurs de chaque noeud)
- trouver les branches complètes pour déterminer les autres branches courtcircuitées
- ex. rule.formule est courtcircuitée si rule.non applicable est vrai
- les feuilles de 'une de ces conditions' sont courtcircuitées si l'une d'elle est vraie
- les feuilles de "toutes ces conditions" sont courtcircuitées si l'une d'elle est fausse
- ...
(- bonus : utiliser ces informations pour l'ordre de priorité des variables inconnues)
- si une branche est incomplète et qu'elle est de type numérique, déterminer les bornes si c'est possible.
Ex. - pour une multiplication, si l'assiette est connue mais que l 'applicabilité est inconnue,
les bornes seront [0, multiplication.value = assiette * taux]
- si taux = effectif entreprise >= 20 ? 1% : 2% et que l'applicabilité est connue,
bornes = [assiette * 1%, assiette * 2%]
- transformer l'arbre en JSX pour afficher le calcul *et son état en prenant en compte les variables renseignées et calculées* de façon sympathique dans un butineur Web tel que Mozilla Firefox.
- surement plein d'autres applications...
*/
export let parseAll = flatRules => {
/* First we parse each rule one by one. When a mechanism is encountered, it is
recursively parsed. When a reference to a variable is encountered, a
'variable' node is created, we don't parse variables recursively. */
let parsedRules = {}
/* A rule `A` can disable a rule `B` using the rule `rend non applicable: B`
in the definition of `A`. We need to map these exonerations to be able to
retreive them from `B` */
let nonApplicableMapping: Record<string, any> = {}
let replacedByMapping: Record<string, any> = {}
flatRules.forEach(rule => {
const parsed = parseRule(flatRules, rule, parsedRules)
if (parsed['rend non applicable']) {
nonApplicableMapping[rule.dottedName] = parsed['rend non applicable']
}
const replaceDescriptors = parsed['remplace']
if (replaceDescriptors) {
replaceDescriptors.forEach(
descriptor =>
(replacedByMapping[descriptor.referenceName] = [
...(replacedByMapping[descriptor.referenceName] ?? []),
{ ...descriptor, referenceName: rule.dottedName }
])
)
}
})
Object.entries(nonApplicableMapping).forEach(([a, b]) => {
b.forEach(ruleName => {
parsedRules[ruleName].isDisabledBy.push(
parseReference(flatRules, parsedRules[ruleName], parsedRules)(a)
)
})
})
Object.entries(replacedByMapping).forEach(([a, b]) => {
parsedRules[a].replacedBy = b.map(({ referenceName, ...other }) => ({
referenceNode: parseReference(
flatRules,
parsedRules[referenceName],
parsedRules
)(referenceName),
...other
}))
})
/* Then we need to infer units. Since only references to variables have been created, we need to wait for the latter map to complete before starting this job. Consider this example :
A = B * C
B = D / E
C unité km
D unité
E unité km
*
* When parsing A's formula, we don't know the unit of B, since only the final nodes have units (it would be too cumbersome to specify a unit to each variable), and B hasn't been parsed yet.
*
* */
return parsedRules
}
export let getTargets = (target, rules) => {
let multiSimulation = path(['simulateur', 'objectifs'])(target)
let targets = Array.isArray(multiSimulation)
? // On a un simulateur qui définit une liste d'objectifs
multiSimulation
.map(n => disambiguateRuleReference(rules, target, n))
.map(n => findRuleByDottedName(rules, n))
: // Sinon on est dans le cas d'une simple variable d'objectif
[target]
return targets
}
type CacheMeta = {
contextRule: Array<string>
defaultUnits: Array<Unit>
inversionFail?: {
given: string
estimated: string
}
}
export let analyseMany = (
parsedRules,
targetNames,
defaultUnits: Array<string> = []
) => (situationGate: (name: DottedName) => any) => {
// TODO: we should really make use of namespaces at this level, in particular
// setRule in Rule.js needs to get smarter and pass dottedName
const defaultParsedUnits = defaultUnits.map(unit => parseUnit(unit))
let cache = {
_meta: { contextRule: [], defaultUnits: defaultParsedUnits } as CacheMeta
}
let parsedTargets = targetNames.map(t => {
let parsedTarget = findRule(parsedRules, t)
if (!parsedTarget)
throw new Error(
`L'objectif de calcul "${t}" ne semble pas exister dans la base de règles`
)
return parsedTarget
}),
targets = chain(pt => getTargets(pt, parsedRules), parsedTargets).map(
(t): EvaluatedRule =>
cache[t.dottedName] || // This check exists because it is not done in parseRuleRoot's eval, while it is in parseVariable. This should be merged : we should probably call parseVariable here : targetNames could be expressions (hence with filters) TODO
evaluateNode(cache, situationGate, parsedRules, t)
)
let controls = evaluateControls(cache, situationGate, parsedRules)
return { targets, cache, controls }
}
export type Analysis = ReturnType<ReturnType<typeof analyse>>
export let analyse = (parsedRules, target, defaultUnits = []) => {
return analyseMany(parsedRules, [target], defaultUnits)
}

View File

@ -1,5 +1,4 @@
import { Action } from 'Actions/actions'
import { Analysis } from 'Engine/traverse'
import { Unit } from 'Engine/units'
import { defaultTo, identity, omit, without } from 'ramda'
import reduceReducers from 'reduce-reducers'
@ -101,7 +100,7 @@ function updateSituation(
}: {
fieldName: DottedName
value: any
analysis: Analysis | Array<Analysis> | null
analysis: any
}
) {
const goals = goalsFromAnalysis(analysis)
@ -187,7 +186,7 @@ function getCompanySituation(company: Company): Situation {
function simulation(
state: Simulation | null = null,
action: Action,
analysis: Analysis | Array<Analysis> | null,
analysis: any,
existingCompany: Company
): Simulation | null {
if (action.type === 'SET_SIMULATION') {

View File

@ -1,31 +1,29 @@
import {
collectMissingVariablesByTarget,
getNextSteps
} from 'Engine/generateQuestions'
import Engine, { parseRules } from 'Engine'
import { getNextSteps } from 'Engine/generateQuestions'
import {
collectDefaults,
disambiguateExampleSituation,
findRuleByDottedName,
rules as rulesEn,
rulesFr,
disambiguateRuleReference,
splitName
} from 'Engine/rules'
import { analyse, analyseMany, parseAll } from 'Engine/traverse'
} from 'Engine/ruleUtils'
import rules from 'Publicode/rules'
import {
add,
difference,
equals,
fromPairs,
head,
intersection,
isNil,
last,
length,
map,
mergeDeepWith,
negate,
pick,
pipe,
sortBy,
takeWhile,
toPairs,
zipWith
} from 'ramda'
import { useSelector } from 'react-redux'
@ -33,7 +31,17 @@ import { RootState, Simulation } from 'Reducers/rootReducer'
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'
import { DottedName } from 'Types/rule'
import { mapOrApply } from '../utils'
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
let disambiguateExampleSituation: any = (rules, rule) =>
pipe(
toPairs as any,
map(([k, v]) => [
disambiguateRuleReference(rules, rule.dottedName, k),
v
]) as any,
fromPairs
)
// create a "selector creator" that uses deep equal instead of ===
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, equals)
@ -45,17 +53,20 @@ let configSelector = (state: RootState) => state.simulation?.config || {}
// state (for tests, library, etc.), and we default to a side-effect value (for
// hot-reloading on developement). See
// https://github.com/betagouv/mon-entreprise/issues/912
export let flatRulesSelector = (state: RootState) =>
state.rules ?? (state.lang === 'en' ? rulesEn : rulesFr)
// TOTO
const rulesEn = rules
let flatRulesSelector = (state: RootState) =>
state.rules ?? (state.lang === 'en' ? rulesEn : rules)
// We must here compute parsedRules, flatRules, analyse which contains both
// targets and cache objects
export let parsedRulesSelector = createSelector([flatRulesSelector], rules =>
parseAll(rules)
parseRules(rules)
)
export let ruleDefaultsSelector = createSelector([flatRulesSelector], rules =>
collectDefaults(rules)
export let ruleDefaultsSelector = createSelector(
[parsedRulesSelector],
parsedRules => collectDefaults(parsedRules)
)
export let targetNamesSelector = (state: RootState) => {
@ -99,7 +110,7 @@ export let firstStepCompletedSelector = createSelector(
return true
}
const targetIsAnswered = targetNames?.some(targetName => {
const rule = findRuleByDottedName(parsedRules, targetName)
const rule = parsedRules[targetName]
return rule?.formule && targetName in situation
})
return targetIsAnswered
@ -157,33 +168,25 @@ export let validatedSituationBranchesSelector = createSituationBrancheSelector(
validatedSituationSelector
)
export let situationsWithDefaultsSelector = createSelector(
[ruleDefaultsSelector, situationBranchesSelector],
(defaults, situations) =>
mapOrApply(situation => ({ ...defaults, ...situation }), situations)
)
let analyseRule = (parsedRules, ruleDottedName, situationGate, defaultUnits) =>
analyse(parsedRules, ruleDottedName, defaultUnits)(situationGate).targets[0]
let evaluateRule = (parsedRules, ruleDottedName, situation, defaultUnits) =>
new Engine({ rules: parsedRules })
.setDefaultUnits(defaultUnits)
.setSituation(situation)
.evaluate(ruleDottedName)
export let ruleAnalysisSelector = createSelector(
[
parsedRulesSelector,
(_, props: { dottedName: DottedName }) => props.dottedName,
situationsWithDefaultsSelector,
situationBranchesSelector,
state => state.situationBranch || 0,
defaultUnitSelector
],
(rules, dottedName, situations, situationBranch, defaultUnit) => {
return analyseRule(
return evaluateRule(
rules,
dottedName,
dottedName => {
const currentSituation = Array.isArray(situations)
? situations[situationBranch]
: situations
return currentSituation[dottedName]
},
Array.isArray(situations) ? situations[situationBranch] : situations,
[defaultUnit]
)
}
@ -192,7 +195,7 @@ export let ruleAnalysisSelector = createSelector(
let exampleSituationSelector = createSelector(
[
parsedRulesSelector,
situationsWithDefaultsSelector,
situationBranchesSelector,
({ currentExample }) => currentExample
],
(rules, situations, example) =>
@ -200,7 +203,7 @@ let exampleSituationSelector = createSelector(
...(situations[0] || situations),
...disambiguateExampleSituation(
rules,
findRuleByDottedName(rules, example.dottedName)
rules[example.dottedName]
)(example.situation)
}
)
@ -213,15 +216,13 @@ export let exampleAnalysisSelector = createSelector(
],
(rules, dottedName, situation, example) =>
situation &&
analyseRule(
rules,
dottedName,
(dottedName: DottedName) => situation[dottedName],
example?.defaultUnit
)
evaluateRule(rules, dottedName, situation, example?.defaultUnit)
)
let makeAnalysisSelector = (situationSelector: SituationSelectorType) =>
let makeAnalysisSelector = (
situationSelector: SituationSelectorType,
useDefaultValues
) =>
createDeepEqualSelector(
[
parsedRulesSelector,
@ -230,20 +231,22 @@ let makeAnalysisSelector = (situationSelector: SituationSelectorType) =>
defaultUnitSelector
],
(parsedRules, targetNames, situations, defaultUnit) => {
return mapOrApply(
situation =>
analyseMany(parsedRules, targetNames, [defaultUnit])(
(dottedName: DottedName) => {
return situation[dottedName]
}
),
situations
)
return mapOrApply(situation => {
const engine = new Engine({ rules: parsedRules, useDefaultValues })
.setSituation(situation)
.setDefaultUnits([defaultUnit])
return {
targets: targetNames.map(target => engine.evaluate(target)),
cache: engine.getCache(),
controls: engine.controls()
}
}, situations)
}
)
export let analysisWithDefaultsSelector = makeAnalysisSelector(
situationsWithDefaultsSelector
situationBranchesSelector as any,
true
)
export let branchAnalyseSelector = createSelector(
@ -262,19 +265,28 @@ export let branchAnalyseSelector = createSelector(
)
let analysisValidatedOnlySelector = makeAnalysisSelector(
validatedSituationBranchesSelector as SituationSelectorType
validatedSituationBranchesSelector as SituationSelectorType,
false
)
let currentMissingVariablesByTargetSelector = createSelector(
[analysisValidatedOnlySelector],
analyses => {
const variables = mapOrApply(
analysis => collectMissingVariablesByTarget(analysis.targets),
analysis =>
analysis.targets.reduce(
(acc, target) => ({
[target.dottedName]: target.missingVariables,
...acc
}),
{}
),
analyses
)
if (Array.isArray(variables)) {
return variables.reduce((acc, next) => mergeDeepWith(add)(acc, next), {})
}
return variables
}
)
@ -328,7 +340,6 @@ export let nextStepsSelector = createSelector(
nextSteps
)
return nextSteps
}
)

View File

@ -1,4 +1,3 @@
import { Analysis } from 'Engine/traverse'
import {
add,
concat,
@ -91,7 +90,7 @@ const groupByBranche = (cotisations: Array<Cotisation>) => {
cotisationsMap[branche]
])
}
export let analysisToCotisations = (analysis: Analysis) => {
export let analysisToCotisations = (analysis: { cache: Cache }) => {
const variables = [
'contrat salarié . cotisations . salariales',
'contrat salarié . cotisations . patronales'
@ -129,6 +128,6 @@ export let analysisToCotisations = (analysis: Analysis) => {
return cotisations
}
export const analysisToCotisationsSelector = createSelector(
[analysisWithDefaultsSelector],
[analysisWithDefaultsSelector as any],
analysisToCotisations
)

View File

@ -1,4 +1,4 @@
import { getRuleFromAnalysis } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import { compose, filter, fromPairs, map, max, reduce, sort } from 'ramda'
import { createSelector } from 'reselect'
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'

View File

@ -7,7 +7,7 @@ import { IsEmbeddedContext } from 'Components/utils/embeddedContext'
import { Markdown } from 'Components/utils/markdown'
import { ScrollToTop } from 'Components/utils/Scroll'
import { formatValue } from 'Engine/format'
import { getRuleFromAnalysis } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import React, { useContext, useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Trans, useTranslation } from 'react-i18next'

View File

@ -2,17 +2,17 @@ import SearchBar from 'Components/SearchBar'
import React from 'react'
import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux'
import { flatRulesSelector } from 'Selectors/analyseSelectors'
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
import './RulesList.css'
export default function RulesList() {
const flatRules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
return (
<div id="RulesList" className="ui__ container">
<h1>
<Trans>Explorez notre documentation</Trans>
</h1>
<SearchBar showDefaultList={true} rules={flatRules} />
<SearchBar showDefaultList={true} rules={rules} />
</div>
)
}

View File

@ -11,8 +11,8 @@ import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
flatRulesSelector,
nextStepsSelector,
parsedRulesSelector,
ruleAnalysisSelector,
situationSelector
} from 'Selectors/analyseSelectors'
@ -54,7 +54,7 @@ const lauchComputationWhenResultsInViewport = () => {
export default function AideDéclarationIndépendant() {
const dispatch = useDispatch()
const rules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
const company = useSelector(
(state: RootState) => state.inFranceApp.existingCompany
)
@ -213,12 +213,12 @@ function SubSection({
dottedName: sectionDottedName,
hideTitle = false
}: SubSectionProp) {
const flatRules = useSelector(flatRulesSelector)
const parsedRules = useSelector(parsedRulesSelector)
const ruleTitle = useRule(sectionDottedName)?.title
const nextSteps = useSelector(nextStepsSelector)
const situation = useSelector(situationSelector)
const title = hideTitle ? null : ruleTitle
const subQuestions = flatRules.filter(
const subQuestions = Object.values(parsedRules).filter(
({ dottedName, question }) =>
Boolean(question) &&
dottedName.startsWith(sectionDottedName) &&
@ -245,7 +245,7 @@ function SimpleField({ dottedName, question, summary }: SimpleFieldProps) {
const evaluatedRule = useSelector((state: RootState) => {
return ruleAnalysisSelector(state, { dottedName })
})
const rules = useSelector(flatRulesSelector)
const rules = useSelector(parsedRulesSelector)
const value = useSelector(situationSelector)[dottedName]
const [currentValue, setCurrentValue] = useState(value)
const dispatchValue = useCallback(

View File

@ -7,14 +7,13 @@ import 'Components/TargetSelection.css'
import { IsEmbeddedContext } from 'Components/utils/embeddedContext'
import { formatValue } from 'Engine/format'
import RuleInput from 'Engine/RuleInput'
import { getRuleFromAnalysis } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import React, { createContext, useContext, useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
flatRulesSelector,
parsedRulesSelector,
ruleAnalysisSelector,
situationSelector
@ -84,7 +83,7 @@ function SimpleField({ dottedName }: SimpleFieldProps) {
return ruleAnalysisSelector(state, { dottedName })
})
const initialRender = useContext(InitialRenderContext)
const flatRules = useSelector(flatRulesSelector)
const parsedRules = useSelector(parsedRulesSelector)
const value = useSelector(situationSelector)[dottedName]
const onChange = x => dispatch(updateSituation(dottedName, x))
@ -108,7 +107,7 @@ function SimpleField({ dottedName }: SimpleFieldProps) {
className="targetInput"
isTarget
dottedName={dottedName}
rules={flatRules}
rules={parsedRules}
value={value}
onChange={onChange}
useSwitch

View File

@ -5,7 +5,7 @@ import autoEntrepreneurConfig from 'Components/simulationConfigs/auto-entreprene
import StackedBarChart from 'Components/StackedBarChart'
import { ThemeColorsContext } from 'Components/utils/colors'
import { IsEmbeddedContext } from 'Components/utils/embeddedContext'
import { getRuleFromAnalysis } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import { default as React, useContext } from 'react'
import { Helmet } from 'react-helmet'
import { Trans, useTranslation } from 'react-i18next'

View File

@ -5,7 +5,7 @@ import indépendantConfig from 'Components/simulationConfigs/indépendant.yaml'
import StackedBarChart from 'Components/StackedBarChart'
import { ThemeColorsContext } from 'Components/utils/colors'
import { IsEmbeddedContext } from 'Components/utils/embeddedContext'
import { getRuleFromAnalysis } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import { default as React, useContext } from 'react'
import { Helmet } from 'react-helmet'
import { Trans, useTranslation } from 'react-i18next'

View File

@ -1,4 +1,4 @@
import { encodeRuleName } from 'Engine/rules'
import { encodeRuleName } from 'Engine/ruleUtils'
import { map, reduce, toPairs, zipObj } from 'ramda'
import { LegalStatus } from 'Selectors/companyStatusSelectors'
import { DottedName } from 'Types/rule'

View File

@ -1,4 +1,4 @@
import { rules as baseRulesEn, rulesFr as baseRulesFr } from 'Engine/rules'
// TODO : load translation only if en
import 'iframe-resizer'
import React, { useEffect } from 'react'
import { Route, Switch } from 'react-router-dom'
@ -14,7 +14,7 @@ function Router({ language }) {
useEffect(() => {
getSessionStorage()?.setItem('lang', language)
}, [language])
const rules = language === 'en' ? baseRulesEn : baseRulesFr
const rules = language === 'en' ? rules : rules
return (
<Provider
basename="publicodes"

View File

@ -1,3 +1,4 @@
import { Unit } from 'Engine/units'
import jsonRules from './dottednames.json'
export type DottedName = keyof typeof jsonRules
@ -5,14 +6,18 @@ export type Rule = {
dottedName: DottedName
question?: string
unité: string
unit: string
unit: Unit
name?: string
summary?: string
title?: string
defaultValue: any
defaultUnit: Unit
type: string
API: Object
parentDependencies: Array<Rule>
icons: string
formule: any
suggestions: Object
}
// This type should be defined inline by the function evaluating the rule (and

View File

@ -1,93 +0,0 @@
import { expect } from 'chai'
import dedent from 'dedent-js'
import { enrichRule } from 'Engine/rules'
import { safeLoad } from 'js-yaml'
import { rules as realRules } from '../source/engine/rules'
import { analyse, analyseMany, parseAll } from '../source/engine/traverse'
describe('bug-analyse-many', function() {
it('complex inversion with composantes', () => {
let rawRules = dedent`
- nom: net
formule: brut - cotisations
- nom: cotisations
formule:
somme:
- cotisation a .salarié
- cotisation b
- nom: cotisation a
formule:
produit:
assiette: brut
composantes:
- attributs:
par: employeur
taux: 10%
- attributs:
par: salarié
taux: 10%
- nom: cotisation b
formule:
produit:
assiette: brut
composantes:
- attributs:
impôt sur le revenu: x
taux: 10%
- attributs:
impôt sur le revenu: y
taux: 10%
- nom: brut
unité:
formule:
inversion numérique:
avec:
- net
`,
rules = parseAll(safeLoad(rawRules).map(enrichRule)),
stateSelector = name => ({ net: 700 }[name])
const targets = ['brut', 'cotisations']
const many = analyseMany(rules, targets)(stateSelector).targets
const one = analyse(rules, 'cotisations')(stateSelector).targets[0]
expect(many[1].nodeValue).to.be.closeTo(one.nodeValue, 0.1)
})
it('should compute the same contributions if asked with analyseMany or analyse', function() {
const situationSelector = dottedName =>
({
'contrat salarié . rémunération . net de cotisations': 3500,
'auto-entrepreneur': 'non',
'contrat salarié': 'oui',
dirigeant: 'assimilé salarié',
'contrat salarié . ATMP . taux réduit': 'oui',
'contrat salarié . CDD': 'non',
'contrat salarié . frais professionnels . indemnité kilométrique vélo . indemnité vélo active':
'non',
'contrat salarié . rémunération . avantages en nature . montant': 0,
'contrat salarié . temps partiel': 'non',
'établissement . localisation': {},
'contrat salarié . complémentaire santé . part employeur': 50,
'contrat salarié . complémentaire santé . forfait': 50,
'entreprise . effectif': 1,
'entreprise . association non lucrative': 'non'
}[dottedName])
const rules = parseAll(realRules.map(enrichRule))
const targets = [
'contrat salarié . rémunération . brut de base',
'contrat salarié . cotisations . salariales'
]
const analyseManyValue = analyseMany(rules, targets)(situationSelector)
.targets[1]
const analyseValue = analyse(
rules,
'contrat salarié . cotisations . salariales'
)(situationSelector).targets[0]
expect(analyseManyValue.nodeValue).to.equal(analyseValue.nodeValue)
})
})

View File

@ -1,80 +1,71 @@
import { expect } from 'chai'
import { enrichRule } from '../source/engine/rules'
import { analyseMany, parseAll } from '../source/engine/traverse'
import Engine, { parseRules } from 'Engine'
describe('controls', function() {
let rawRules = [
{
nom: 'net',
formule: 'brut - cotisation'
},
{ nom: 'cotisation', formule: 235 },
{ nom: 'résident en France', formule: 'oui' },
{
nom: 'brut',
unité: '€',
question: 'Quel est le salaire brut ?',
contrôles: [
{
si: 'brut < 300',
niveau: 'bloquant',
message:
'Malheureux, je crois que vous vous êtes trompé dans votre saisie.'
let rawRules = {
net: { formule: 'brut - cotisation' },
cotisation: { formule: 235 },
'résident en France': { formule: 'oui' },
brut: {
unité: '€',
question: 'Quel est le salaire brut ?',
contrôles: [
{
si: 'brut < 300',
niveau: 'bloquant',
message:
'Malheureux, je crois que vous vous êtes trompé dans votre saisie.'
},
{
si: 'brut < 1000',
niveau: 'bloquant',
message: 'Toujours pas, nous avons des standards en France !'
},
{
si: 'brut < 1500',
niveau: 'avertissement',
message: 'Toujours pas, nous avons des standards en France !'
},
{
si: 'brut > 100000',
niveau: 'information',
message: 'Oulah ! Oulah !'
},
{
si: {
'toutes ces conditions': ['brut > 1000000', 'résident en France']
},
{
si: 'brut < 1000',
niveau: 'bloquant',
message: 'Toujours pas, nous avons des standards en France !'
},
{
si: 'brut < 1500',
niveau: 'avertissement',
message: 'Toujours pas, nous avons des standards en France !'
},
{
si: 'brut > 100000',
niveau: 'information',
message: 'Oulah ! Oulah !'
},
{
si: {
'toutes ces conditions': ['brut > 1000000', 'résident en France']
},
niveau: 'information',
message: 'Vous êtes un contribuable hors-pair !'
}
]
}
],
rules = rawRules.map(enrichRule),
parsedRules = parseAll(rules)
niveau: 'information',
message: 'Vous êtes un contribuable hors-pair !'
}
]
}
}
it('Should parse blocking controls', function() {
let controls = Object.values(parsedRules).find(r => r.contrôles).contrôles
let controls = Object.values(parseRules(rawRules)).find(r => r.contrôles)
.contrôles
expect(
controls.filter(({ level }) => level == 'bloquant')
).to.have.lengthOf(2)
})
it('Should allow imbricated conditions', function() {
let controls = analyseMany(parsedRules, ['net'])(
dottedName => ({ brut: 2000000 }[dottedName])
).controls
const engine = new Engine({ rules: rawRules })
let controls = engine.setSituation({ brut: 2000000 }).controls()
expect(
controls.find(
({ message }) => message === 'Vous êtes un contribuable hors-pair !'
)
).to.exist
let controls2 = analyseMany(parsedRules, ['net'])(
dottedName => ({ brut: 100001 }[dottedName])
).controls
let controls2 = engine.setSituation({ brut: 100001 }).controls()
expect(controls2.find(({ message }) => message === 'Oulah ! Oulah !')).to
.exist
let controls3 = analyseMany(parsedRules, ['net'])(
dottedName => ({ brut: 100 }[dottedName])
).controls
let controls3 = engine.setSituation({ brut: 100 }).controls()
expect(
controls3.find(
({ message }) =>

View File

@ -1,5 +1,5 @@
import { expect } from 'chai'
import { enrichRule, rulesFr as rules } from 'Engine/rules'
import rules from 'Publicode/rules'
import { assocPath, merge } from 'ramda'
import reducers from 'Reducers/rootReducer'
import salariéConfig from '../source/components/simulationConfigs/salarié.yaml'
@ -13,20 +13,19 @@ let baseState = {
describe('conversation', function() {
it('should start with the first missing variable', function() {
let rawRules = [
let rules = {
// TODO - this won't work without the indirection, figure out why
{ nom: 'top . startHere', formule: { somme: ['a', 'b'] } },
{ nom: 'top . a', formule: 'aa' },
{ nom: 'top . b', formule: 'bb' },
{ nom: 'top . aa', question: '?', titre: 'a', unité: '€' },
{ nom: 'top . bb', question: '?', titre: 'b', unité: '€' }
],
rules = rawRules.map(enrichRule),
'top . startHere': { formule: { somme: ['a', 'b'] } },
'top . a': { formule: 'aa' },
'top . b': { formule: 'bb' },
'top . aa': { question: '?', titre: 'a', unité: '€' },
'top . bb': { question: '?', titre: 'b', unité: '€' }
},
state = merge(baseState, {
rules,
simulation: {
defaultUnit: '€/an',
config: { objectifs: ['startHere'] },
config: { objectifs: ['top . startHere'] },
foldedSteps: []
}
}),
@ -35,26 +34,24 @@ describe('conversation', function() {
expect(currentQuestion).to.equal('top . aa')
})
it('should deal with double unfold', function() {
let rawRules = [
// TODO - this won't work without the indirection, figure out why
{
nom: 'top . startHere',
formule: { somme: ['a', 'b', 'c'] }
},
{ nom: 'top . a', formule: 'aa' },
{ nom: 'top . b', formule: 'bb' },
{ nom: 'top . c', formule: 'cc' },
{ nom: 'top . aa', question: '?', titre: 'a', unité: '€' },
{ nom: 'top . bb', question: '?', titre: 'b', unité: '€' },
{ nom: 'top . cc', question: '?', titre: 'c', unité: '€' }
],
rules = rawRules.map(enrichRule)
let rules = {
// TODO - this won't work without the indirection, figure out why
'top . startHere': {
formule: { somme: ['a', 'b', 'c'] }
},
'top . a': { formule: 'aa' },
'top . b': { formule: 'bb' },
'top . c': { formule: 'cc' },
'top . aa': { question: '?', titre: 'a', unité: '€' },
'top . bb': { question: '?', titre: 'b', unité: '€' },
'top . cc': { question: '?', titre: 'c', unité: '€' }
}
let step1 = merge(baseState, {
rules,
simulation: {
defaultUnit: '€/an',
config: { objectifs: ['startHere'] },
config: { objectifs: ['top . startHere'] },
foldedSteps: []
}
})
@ -96,40 +93,36 @@ describe('conversation', function() {
})
it('should first ask for questions without defaults, then those with defaults', function() {
let rawRules = [
{ nom: 'net', formule: 'brut - cotisation' },
{
nom: 'brut',
question: 'Quel est le salaire brut ?'
},
{
nom: 'cotisation',
formule: {
produit: {
assiette: 'brut',
variations: [
{
si: 'cadre',
alors: {
taux: '77%'
}
},
{
sinon: {
taux: '80%'
}
let rules = {
net: { formule: 'brut - cotisation' },
brut: {
question: 'Quel est le salaire brut ?'
},
cotisation: {
formule: {
produit: {
assiette: 'brut',
variations: [
{
si: 'cadre',
alors: {
taux: '77%'
}
]
}
},
{
sinon: {
taux: '80%'
}
}
]
}
},
{
nom: 'cadre',
question: 'Est-ce un cadre ?',
'par défaut': 'non'
}
],
rules = rawRules.map(enrichRule)
},
cadre: {
question: 'Est-ce un cadre ?',
'par défaut': 'non'
}
}
let step1 = merge(baseState, {
rules,

View File

@ -1,6 +1,7 @@
import { expect } from 'chai'
import salariéConfig from 'Components/simulationConfigs/salarié.yaml'
import { getRuleFromAnalysis, rules } from 'Engine/rules'
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
import rules from 'Publicode/rules'
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
import {
analysisToCotisationsSelector,

View File

@ -1,285 +1,267 @@
import { expect } from 'chai'
import {
collectMissingVariables,
getNextSteps
} from '../source/engine/generateQuestions'
import { enrichRule, rules as realRules } from '../source/engine/rules'
import { analyse, parseAll } from '../source/engine/traverse'
import Engine from 'Engine'
import rules from 'Publicode/rules'
import { getNextSteps } from '../source/engine/generateQuestions'
let stateSelector = () => undefined
describe('collectMissingVariables', function() {
describe('Missing variables', function() {
it('should identify missing variables', function() {
let rawRules = [
{ nom: 'sum' },
{
nom: 'sum . startHere',
formule: 2,
'non applicable si': 'sum . evt . ko'
},
{
nom: 'sum . evt',
formule: { 'une possibilité': ['ko'] },
titre: 'Truc',
question: '?'
},
{ nom: 'sum . evt . ko' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
sum: {},
'sum . startHere': {
formule: 2,
'non applicable si': 'sum . evt . ko'
},
'sum . evt': {
formule: { 'une possibilité': ['ko'] },
titre: 'Truc',
question: '?'
},
'sum . evt . ko': {}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('sum . startHere')
.missingVariables
)
expect(result).to.include('sum . evt . ko')
})
it('should identify missing variables mentioned in expressions', function() {
let rawRules = [
{ nom: 'sum' },
{ nom: 'sum . evt' },
{
nom: 'sum . startHere',
formule: 2,
'non applicable si': 'evt . nyet > evt . nope'
},
{ nom: 'sum . evt . nope' },
{ nom: 'sum . evt . nyet' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
sum: {},
'sum . evt': {},
'sum . startHere': {
formule: 2,
'non applicable si': 'evt . nyet > evt . nope'
},
'sum . evt . nope': {},
'sum . evt . nyet': {}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('sum . startHere')
.missingVariables
)
expect(result).to.include('sum . evt . nyet')
expect(result).to.include('sum . evt . nope')
})
it('should ignore missing variables in the formula if not applicable', function() {
let rawRules = [
{ nom: 'sum' },
{
nom: 'sum . startHere',
formule: 'trois',
'non applicable si': '3 > 2'
},
{ nom: 'sum . trois' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
sum: {},
'sum . startHere': {
formule: 'trois',
'non applicable si': '3 > 2'
},
'sum . trois': {}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('sum . startHere')
.missingVariables
)
expect(result).to.be.empty
})
it('should not report missing variables when "one of these" short-circuits', function() {
let rawRules = [
{ nom: 'sum' },
{
nom: 'sum . startHere',
formule: 'trois',
'non applicable si': {
'une de ces conditions': ['3 > 2', 'trois']
}
},
{ nom: 'sum . trois' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
sum: {},
'sum . startHere': {
formule: 'trois',
'non applicable si': {
'une de ces conditions': ['3 > 2', 'trois']
}
},
'sum . trois': {}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('sum . startHere')
.missingVariables
)
expect(result).to.be.empty
})
it('should report "une possibilité" as a missing variable even though it has a formula', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: 'trois' },
{
nom: 'top . trois',
formule: { 'une possibilité': ['ko'] }
}
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
top: {},
'top . startHere': { formule: 'trois' },
'top . trois': {
formule: { 'une possibilité': ['ko'] }
}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('top . startHere')
.missingVariables
)
expect(result).to.include('top . trois')
})
it('should not report missing variables when "une possibilité" is inapplicable', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: 'trois' },
{
nom: 'top . trois',
formule: { 'une possibilité': ['ko'] },
'non applicable si': 1
}
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
top: {},
'top . startHere': { formule: 'trois' },
'top . trois': {
formule: { 'une possibilité': ['ko'] },
'non applicable si': 1
}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('top . startHere')
.missingVariables
)
expect(result).to.be.empty
null
})
it('should not report missing variables when "une possibilité" was answered', function() {
let mySelector = name => ({ 'top . trois': 'ko' }[name])
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: 'trois' },
{
nom: 'top . trois',
formule: { 'une possibilité': ['ko'] }
}
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(mySelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
top: {},
'top . startHere': { formule: 'trois' },
'top . trois': {
formule: { 'une possibilité': ['ko'] }
}
}
const result = Object.keys(
new Engine({ rules: rawRules })
.setSituation({ 'top . trois': 'ko' })
.evaluate('top . startHere').missingVariables
)
expect(result).to.be.empty
})
// TODO : enlever ce test, depuis que l'on évalue plus les branches qui ne sont pas encore applicable
// TODO : réparer ce test
it.skip('should report missing variables in variations', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'top . startHere',
formule: { somme: ['variations'] }
},
{
nom: 'top . variations',
formule: {
barème: {
assiette: 2008,
variations: [
{
si: 'dix',
alors: {
multiplicateur: 'deux',
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 'trois' },
{ taux: 10 }
]
}
},
{
si: '3 > 4',
alors: {
multiplicateur: 'quatre',
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 1.8 },
{ 'au-dessus de': 2, taux: 10 }
]
}
const rawRules = {
top: {},
'top . startHere': {
formule: { somme: ['variations'] }
},
'top . variations': {
formule: {
barème: {
assiette: 2008,
variations: [
{
si: 'dix',
alors: {
multiplicateur: 'deux',
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 'trois' },
{ taux: 10 }
]
}
]
}
},
{
si: '3 > 4',
alors: {
multiplicateur: 'quatre',
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 1.8 },
{ 'au-dessus de': 2, taux: 10 }
]
}
}
]
}
},
{ nom: 'top . dix' },
{ nom: 'top . deux' },
{ nom: 'top . trois' },
{ nom: 'top . quatre' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
}
},
'top . dix': {},
'top . deux': {},
'top . trois': {},
'top . quatre': {}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('top . startHere')
.missingVariables
)
expect(result).to.include('top . dix')
expect(result).to.include('top . deux')
expect(result).to.include('top . trois')
expect(result).not.to.include('top . quatre')
// TODO
// expect(result).to.include('top . trois')
})
})
describe('nextSteps', function() {
it('should generate questions for simple situations', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . sum', formule: 'deux' },
{
nom: 'top . deux',
formule: 2,
'non applicable si': 'top . sum . evt'
},
{
nom: 'top . sum . evt',
titre: 'Truc',
question: '?'
}
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'sum')(stateSelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
top: {},
'top . sum': { formule: 'deux' },
'top . deux': {
formule: 2,
'non applicable si': 'top . sum . evt'
},
'top . sum . evt': {
titre: 'Truc',
question: '?'
}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables
)
expect(result).to.have.lengthOf(1)
expect(result[0]).to.equal('top . sum . evt')
})
it('should generate questions', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . sum', formule: 'deux' },
{
nom: 'top . deux',
formule: 'sum . evt'
},
{
nom: 'top . sum . evt',
question: '?'
}
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'sum')(stateSelector),
result = collectMissingVariables(analysis.targets)
const rawRules = {
top: {},
'top . sum': { formule: 'deux' },
'top . deux': {
formule: 'sum . evt'
},
'top . sum . evt': {
question: '?'
}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables
)
expect(result).to.have.lengthOf(1)
expect(result[0]).to.equal('top . sum . evt')
})
// todo : réflechir à l'applicabilité de ce test
it.skip('should generate questions with more intricate situation', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . sum', formule: { somme: [2, 'deux'] } },
{
nom: 'top . deux',
formule: 2,
'non applicable si': "top . sum . evt = 'ko'"
},
{
nom: 'top . sum . evt',
formule: { 'une possibilité': ['ko'] },
titre: 'Truc',
question: '?'
},
{ nom: 'top . sum . evt . ko' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'sum')(stateSelector),
result = collectMissingVariables(analysis.targets)
expect(result).to.have.lengthOf(2)
expect(result).to.eql(['top . sum', 'top . sum . evt'])
it('should generate questions with more intricate situation', function() {
const rawRules = {
top: {},
'top . sum': { formule: { somme: [2, 'deux'] } },
'top . deux': {
formule: 2,
'non applicable si': "top . sum . evt = 'ko'"
},
'top . sum . evt': {
formule: { 'une possibilité': ['ko'] },
titre: 'Truc',
question: '?'
},
'top . sum . evt . ko': {}
}
const result = Object.keys(
new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables
)
expect(result).to.eql(['top . sum . evt'])
})
it('should ask "motif CDD" if "CDD" applies', function() {
let stateSelector = name =>
({
'contrat salarié': 'oui',
'contrat salarié . CDD': 'oui',
'contrat salarié . rémunération . brut de base': '2300'
}[name])
let rules = parseAll(realRules.map(enrichRule)),
analysis = analyse(
rules,
'contrat salarié . rémunération . net'
)(stateSelector),
result = collectMissingVariables(analysis.targets)
const result = Object.keys(
new Engine({ rules, useDefaultValues: false })
.setSituation({
'contrat salarié': 'oui',
'contrat salarié . CDD': 'oui',
'contrat salarié . rémunération . brut de base': '2300'
})
.evaluate('contrat salarié . rémunération . net').missingVariables
)
expect(result).to.include('contrat salarié . CDD . motif')
})

View File

@ -1,85 +1,73 @@
import { expect } from 'chai'
import dedent from 'dedent-js'
import { safeLoad } from 'js-yaml'
import { collectMissingVariables } from '../source/engine/generateQuestions'
import { enrichRule, rules as realRules } from '../source/engine/rules'
import { analyse, analyseMany, parseAll } from '../source/engine/traverse'
import Engine from 'Engine'
describe('inversions', () => {
it('should handle non inverted example', () => {
let fakeState = { brut: 2300 }
let stateSelector = name => fakeState[name]
let rawRules = dedent`
- nom: net
const rules = dedent`
net:
formule:
produit:
assiette: brut
taux: 77%
- nom: brut
brut:
unité:
`,
rules = parseAll(safeLoad(rawRules).map(enrichRule)),
analysis = analyse(rules, 'net')(stateSelector)
`
const result = new Engine({ rules })
.setSituation({ brut: 2300 })
.evaluate('net')
expect(analysis.targets[0].nodeValue).to.be.closeTo(1771, 0.001)
expect(result.nodeValue).to.be.closeTo(1771, 0.001)
})
it('should handle simple inversion', () => {
let fakeState = { net: 2000 }
let stateSelector = name => fakeState[name]
let rawRules = dedent`
- nom: net
const rules = dedent`
net:
formule:
produit:
assiette: brut
taux: 77%
- nom: brut
brut:
unité:
formule:
inversion numérique:
avec:
- net
`,
rules = parseAll(safeLoad(rawRules).map(enrichRule)),
analysis = analyse(rules, 'brut')(stateSelector)
`
const result = new Engine({ rules })
.setSituation({ net: 2000 })
.evaluate('brut')
expect(analysis.targets[0].nodeValue).to.be.closeTo(
2000 / (77 / 100),
0.0001 * 2000
)
expect(result.nodeValue).to.be.closeTo(2000 / (77 / 100), 0.0001 * 2000)
})
it('should handle inversion with value at 0', () => {
let fakeState = { net: 0 }
let stateSelector = name => fakeState[name]
let rawRules = dedent`
- nom: net
const rules = dedent`
net:
formule:
produit:
assiette: brut
taux: 77%
- nom: brut
brut:
unité:
formule:
inversion numérique:
avec:
- net
`,
rules = parseAll(safeLoad(rawRules).map(enrichRule)),
analysis = analyse(rules, 'brut')(stateSelector)
`
const result = new Engine({ rules })
.setSituation({ net: 0 })
.evaluate('brut')
expect(analysis.targets[0].nodeValue).to.be.closeTo(0, 0.0001)
expect(result.nodeValue).to.be.closeTo(0, 0.0001)
})
it('should ask the input of one of the possible inversions', () => {
let rawRules = dedent`
- nom: net
const rules = dedent`
net:
formule:
produit:
assiette: assiette
@ -90,29 +78,26 @@ describe('inversions', () => {
- sinon:
taux: 70%
- nom: brut
brut:
unité:
formule:
inversion numérique:
avec:
- net
- nom: cadre
- nom: assiette
cadre:
assiette:
formule: 67 + brut
`,
rules = parseAll(safeLoad(rawRules).map(enrichRule)),
stateSelector = () => null,
analysis = analyse(rules, 'brut')(stateSelector),
missing = collectMissingVariables(analysis.targets)
`
const result = new Engine({ rules }).evaluate('brut')
expect(analysis.targets[0].nodeValue).to.be.null
expect(missing).to.include('brut')
expect(result.nodeValue).to.be.null
expect(Object.keys(result.missingVariables)).to.include('brut')
})
it('should handle inversions with missing variables', () => {
let rawRules = dedent`
- nom: net
const rules = dedent`
net:
formule:
produit:
assiette: assiette
@ -123,23 +108,23 @@ describe('inversions', () => {
- sinon:
taux: 70%
- nom: brut
brut:
unité:
formule:
inversion numérique:
avec:
- net
- nom: cadre
- nom: assiette
cadre:
assiette:
formule:
somme:
- 1200
- brut
- taxeOne
- nom: taxeOne
taxeOne:
non applicable si: cadre
formule: taxe + taxe
- nom: taxe
taxe:
formule:
produit:
assiette: 1200
@ -149,19 +134,17 @@ describe('inversions', () => {
taux: 80%
- sinon:
taux: 70%
`,
rules = parseAll(safeLoad(rawRules).map(enrichRule)),
stateSelector = name => ({ net: 2000 }[name]),
analysis = analyse(rules, 'brut')(stateSelector),
missing = collectMissingVariables(analysis.targets)
expect(analysis.targets[0].nodeValue).to.be.null
expect(missing).to.include('cadre')
`
const result = new Engine({ rules })
.setSituation({ net: 2000 })
.evaluate('brut')
expect(result.nodeValue).to.be.null
expect(Object.keys(result.missingVariables)).to.include('cadre')
})
it("shouldn't report a missing salary if another salary was input", () => {
let rawRules = dedent`
- nom: net
const rules = dedent`
net:
formule:
produit:
assiette: assiette
@ -173,13 +156,13 @@ describe('inversions', () => {
alors:
taux: 70%
- nom: total
total:
formule:
produit:
assiette: assiette
taux: 150%
- nom: brut
brut:
unité:
formule:
inversion numérique:
@ -187,29 +170,28 @@ describe('inversions', () => {
- net
- total
- nom: cadre
cadre:
- nom: assiette
assiette:
formule: 67 + brut
`,
rules = parseAll(safeLoad(rawRules).map(enrichRule)),
stateSelector = name => ({ net: 2000, cadre: 'oui' }[name]),
analysis = analyse(rules, 'total')(stateSelector),
missing = collectMissingVariables(analysis.targets)
expect(analysis.targets[0].nodeValue).to.be.closeTo(3750, 1)
expect(missing).to.be.empty
`
const result = new Engine({ rules })
.setSituation({ net: 2000, cadre: 'oui' })
.evaluate('total')
expect(result.nodeValue).to.be.closeTo(3750, 1)
expect(Object.keys(result.missingVariables)).to.be.empty
})
it('complex inversion with composantes', () => {
let rawRules = dedent`
- nom: net
const rules = dedent`
net:
formule:
produit:
assiette: 67 + brut
taux: 80%
- nom: cotisation
cotisation:
formule:
produit:
assiette: 67 + brut
@ -221,38 +203,21 @@ describe('inversions', () => {
par: salarié
taux: 50%
- nom: total
total:
formule: cotisation .employeur + cotisation .salarié
- nom: brut
brut:
unité:
formule:
inversion numérique:
avec:
- net
- total
`,
rules = parseAll(safeLoad(rawRules).map(enrichRule)),
stateSelector = name => ({ net: 2000 }[name]),
analysis = analyse(rules, 'total')(stateSelector),
missing = collectMissingVariables(analysis.targets)
expect(analysis.targets[0].nodeValue).to.be.closeTo(3750, 1)
expect(missing).to.be.empty
})
it('should collect missing variables not too slowly', function() {
let stateSelector = name =>
({ 'contrat salarié . rémunération . net': '2300' }[name])
let rules = parseAll(realRules.map(enrichRule)),
analysis = analyseMany(rules, [
'contrat salarié . rémunération . brut',
'contrat salarié . rémunération . total'
])(stateSelector)
let start = Date.now()
collectMissingVariables(analysis.targets)
let elapsed = Date.now() - start
expect(elapsed).to.be.below(1500)
`
const result = new Engine({ rules })
.setSituation({ net: 2000 })
.evaluate('total')
expect(result.nodeValue).to.be.closeTo(3750, 1)
expect(Object.keys(result.missingVariables)).to.be.empty
})
})

View File

@ -1,4 +1,5 @@
import { expect } from 'chai'
import rules from 'Publicode/rules'
import Engine from '../source/engine/index'
import co2 from './rules/co2.yaml'
import sasuRules from './rules/sasu.yaml'
@ -6,7 +7,7 @@ import sasuRules from './rules/sasu.yaml'
describe('library', function() {
it('should evaluate one target with no input data', function() {
let target = 'contrat salarié . rémunération . net'
let engine = new Engine()
let engine = new Engine({ rules })
engine.setSituation({
'contrat salarié . rémunération . brut de base': 2300
})
@ -28,7 +29,7 @@ yi:
expect(engine.evaluate('yi').nodeValue).to.equal(202)
})
it('should let the user add rules to the default ones', function() {
it.skip('should let the user add rules to the default ones', function() {
let rules = `
yo:
formule: 1
@ -42,33 +43,36 @@ ya:
expect(engine.evaluate('ya').nodeValue).to.be.closeTo(1799, 1)
})
it('should let the user extend the rules constellation in a serious manner', function() {
let CA = 550 * 16
let engine = new Engine({ extra: sasuRules })
engine.setSituation({
'chiffre affaires': CA
})
let salaireTotal = engine.evaluate('salaire total').nodeValue
it.skip(
'should let the user extend the rules constellation in a serious manner',
function() {
let CA = 550 * 16
let engine = new Engine({ extra: sasuRules })
engine.setSituation({
'chiffre affaires': CA
})
let salaireTotal = engine.evaluate('salaire total').nodeValue
engine.setSituation({
'contrat salarié . prix du travail': salaireTotal
})
let salaireNetAprèsImpôt = engine.evaluate(
'contrat salarié . rémunération . net après impôt'
).nodeValue
engine.setSituation({
'contrat salarié . prix du travail': salaireTotal
})
let salaireNetAprèsImpôt = engine.evaluate(
'contrat salarié . rémunération . net après impôt'
).nodeValue
engine.setSituation({
'contrat salarié . rémunération . net après impôt': salaireNetAprèsImpôt,
'chiffre affaires': CA
})
let [revenuDisponible, dividendes] = engine.evaluate([
'contrat salarié . rémunération . net après impôt',
'dividendes . net'
])
engine.setSituation({
'contrat salarié . rémunération . net après impôt': salaireNetAprèsImpôt,
'chiffre affaires': CA
})
let [revenuDisponible, dividendes] = engine.evaluate([
'contrat salarié . rémunération . net après impôt',
'dividendes . net'
])
expect(revenuDisponible.nodeValue).to.be.closeTo(2324, 1)
expect(dividendes.nodeValue).to.be.closeTo(2507, 1)
}).timeout(5000)
expect(revenuDisponible.nodeValue).to.be.closeTo(2324, 1)
expect(dividendes.nodeValue).to.be.closeTo(2507, 1)
}
).timeout(5000)
it('should let the user define a simplified revenue tax system', function() {
let rules = `

View File

@ -6,62 +6,57 @@
*/
import { expect } from 'chai'
import { collectMissingVariables } from '../source/engine/generateQuestions'
import { enrichRule } from '../source/engine/rules'
import { analyse, parseAll } from '../source/engine/traverse'
import Engine from 'Engine'
import { parseUnit } from '../source/engine/units'
import testSuites from './load-mecanism-tests'
describe('Mécanismes', () =>
testSuites.map(([suiteName, suite]) =>
Object.keys(suite)
.map(key => [key, suite[key] ?? undefined])
.map(
([name, { exemples, titre, 'unité attendue': unit } = {}]) =>
exemples &&
describe(`Suite ${suiteName}, test : ${titre ?? name}`, () =>
exemples.map(
({
nom: testTexte,
situation,
'unités par défaut': defaultUnits,
'valeur attendue': valeur,
'variables manquantes': expectedMissing
}) =>
it(testTexte == null ? '' : testTexte + '', () => {
let rules = parseAll(
Object.entries(suite)
.map(([dottedName, rule]) => ({
dottedName,
...rule
}))
.map(enrichRule)
),
state = situation || {},
stateSelector = name => state[name],
analysis = analyse(
rules,
name,
defaultUnits
)(stateSelector),
missing = collectMissingVariables(analysis.targets),
target = analysis.targets[0]
if (typeof valeur === 'number') {
expect(target.nodeValue).to.be.closeTo(valeur, 0.001)
} else if (valeur !== undefined) {
expect(target).to.have.property('nodeValue', valeur)
}
if (expectedMissing) {
expect(missing).to.eql(expectedMissing)
}
if (unit) {
expect(target.unit).not.to.be.equal(undefined)
expect(target.unit).to.deep.equal(parseUnit(unit))
}
})
))
)
))
testSuites.forEach(([suiteName, suite]) => {
const engine = new Engine({ rules: suite, useDefaultValues: false })
describe(`Mécanisme ${suiteName}`, () => {
Object.entries(suite)
.filter(([, rule]) => rule?.exemples)
.forEach(([name, test]) => {
const { exemples, 'unité attendue': unit } = test
exemples.forEach(
(
{
nom: testName,
situation,
'unités par défaut': defaultUnits,
'valeur attendue': valeur,
'variables manquantes': expectedMissing
},
i
) => {
it(
name +
(testName
? ` [${testName}]`
: exemples.length > 1
? ` (${i + 1})`
: ''),
() => {
const result = engine
.setSituation(situation ?? {})
.setDefaultUnits(defaultUnits)
.evaluate(name)
if (typeof valeur === 'number') {
expect(result.nodeValue).to.be.closeTo(valeur, 0.001)
} else if (valeur !== undefined) {
expect(result.nodeValue).to.be.eq(valeur)
}
if (expectedMissing) {
expect(Object.keys(result.missingVariables)).to.eql(
expectedMissing
)
}
if (unit) {
expect(result.unit).not.to.be.equal(undefined)
expect(result.unit).to.deep.equal(parseUnit(unit))
}
}
)
}
)
})
})
})

View File

@ -117,7 +117,6 @@ Conversion dans une comparaison:
mutuelle:
formule: 30 €/mois
retraite:
formule:
produit:

View File

@ -25,9 +25,7 @@ plafonnement reference inactive:
exemples:
- valeur attendue: 1000
plafonnement reference inactive . plafond:
formule: non
plafonnement reference inactive . plafond: non
plancher:
formule:
encadrement:

View File

@ -1,12 +1,9 @@
salaire brut:
formule: 2000
salaire net:
formule: 0.5 * salaire brut
SMIC brut:
formule: 1000
SMIC net:
formule:
recalcul:

View File

@ -1,9 +1,7 @@
restaurant . prix du repas:
formule: 10 €/repas
restaurant . client gourmand:
formule: oui
restaurant . client enfant:
rend non applicable:
- client gourmand
@ -36,7 +34,6 @@ modifie une règle:
cotisations . assiette:
formule: 1000
cotisations:
formule:
somme:
@ -157,7 +154,6 @@ exemple5:
A:
formule: 1
B:
remplace: A
formule: 2
@ -173,14 +169,12 @@ remplacement associatif:
x:
formule: non
x . y:
remplace: z
formule: 10
formule: 20
z:
formule: 1
remplacement non applicable (branche desactivée):
formule: z
exemples:

View File

@ -1,6 +1,5 @@
impôt:
formule: 1000
exilé fiscal:
rend non applicable:
- impôt

View File

@ -53,10 +53,8 @@ heures d'absences:
temps contractuel:
formule: 145 heures/mois
temps de travail effectif:
formule: temps contractuel - heures d'absences
plafond sécurité sociale proratisé:
formule:
multiplication:

View File

@ -1,6 +1,5 @@
variable temporelle numérique . le . valeur:
formule: 40 €/mois | le 02/04/2019
variable temporelle numérique . le . test date applicable:
formule: valeur | le 02/04/2019
exemples:
@ -13,7 +12,6 @@ variable temporelle numérique . le . test date non applicable:
variable temporelle numérique . depuis . valeur:
formule: 40 €/mois | depuis le 02/04/2019
variable temporelle numérique . depuis . test date applicable:
formule: valeur | depuis le 06/04/2019
exemples:
@ -26,7 +24,6 @@ variable temporelle numérique . depuis . test date non applicable:
variable temporelle numérique . intervalle . valeur:
formule: 40 €/mois | du 02/04/2019 | au 04/05/2020
variable temporelle numérique . intervalle . test date applicable:
formule: valeur | le 06/04/2019
exemples:
@ -49,10 +46,8 @@ variable temporelle numérique . intervalle . test date non applicable 2:
variable temporelle numérique . variable . date limite de paiement:
formule: 03/09/2020
variable temporelle numérique . variable . majorations de retard:
formule: '40 €/jour | à partir de : date limite de paiement'
variable temporelle numérique . variable . test date non applicable:
formule: "majorations de retard | jusqu'au : 02/09/2020"
exemples:
@ -75,7 +70,6 @@ variable temporelle numérique . variable . test date applicable 2:
prix:
formule: (20 €/mois | à partir du 15/11/2019) + (10 €/mois | à partir du 01/02/2020)
date:
variable temporelle numérique . test addition:
formule: 'prix | le : date'
@ -92,7 +86,6 @@ variable temporelle numérique . test addition:
prix avec variations:
formule: prix * (50% | du 01/01/2020 | au 31/01/2020)
début:
fin:
variable temporelle numérique . expression . multiplication:
@ -144,7 +137,6 @@ variable temporelle numérique . variation:
contrat salarié . date d'embauche:
formule: 12/09/2018
contrat salarié . salaire:
formule:
somme:
@ -159,7 +151,6 @@ contrat salarié . salaire . brut de base:
contrat salarié . salaire . primes:
formule: 2000€/mois | du 01/12/2019 | au 31/12/2019
plafond sécurité sociale:
formule:
somme:

View File

@ -1,8 +1,8 @@
import { AssertionError } from 'chai'
import { parseRules } from 'Engine'
import rules from 'Publicode/rules'
import { merge } from 'ramda'
import { exampleAnalysisSelector } from 'Selectors/analyseSelectors'
import { rules } from '../source/engine/rules'
import { parseAll } from '../source/engine/traverse'
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
@ -34,7 +34,7 @@ let runExamples = (examples, rule) =>
})
})
let parsedRules = parseAll(rules)
let parsedRules = parseRules(rules)
describe('Tests des règles de notre base de règles', () =>
Object.values(parsedRules).map(rule => {
if (!rule.exemples) return null

View File

@ -6,7 +6,7 @@ salarié:
bnc:
- artiste-auteur . revenus . BNC . recettes: 10000
- artiste-auteur . revenus . BNC . recettes: 10000
artiste-auteur . revenus . BNC . micro-bnc: false
artiste-auteur . revenus . BNC . micro-bnc: non
- artiste-auteur . revenus . BNC . recettes: 10000
artiste-auteur . revenus . BNC . micro-bnc: false
artiste-auteur . revenus . BNC . micro-bnc: non
artiste-auteur . revenus . BNC . frais réels: 5000

View File

@ -12,14 +12,14 @@
aides:
- dirigeant . auto-entrepreneur . net de cotisations: 5000
entreprise . ACRE: true
entreprise . ACRE: oui
- dirigeant . auto-entrepreneur . net de cotisations: 50000
entreprise . ACRE: true
entreprise . ACRE: oui
impôt sur le revenu:
- dirigeant . auto-entrepreneur . net de cotisations: 25000
entreprise . catégorie d'activité: 'libérale'
dirigeant . auto-entrepreneur . impôt . versement libératoire: true
dirigeant . auto-entrepreneur . impôt . versement libératoire: oui
ACRE:
- dirigeant . auto-entrepreneur . net de cotisations: 20000

View File

@ -30,7 +30,7 @@ activités:
entreprise . catégorie d'activité: libérale
- dirigeant . rémunération totale: 20000
entreprise . catégorie d'activité: libérale
entreprise . catégorie d'activité . libérale règlementée: true
entreprise . catégorie d'activité . libérale règlementée: oui
- dirigeant . rémunération totale: 20000
entreprise . catégorie d'activité: artisanale
- dirigeant . rémunération totale: 20000
@ -39,4 +39,4 @@ activités:
- dirigeant . rémunération totale: 20000
entreprise . catégorie d'activité: commerciale ou industrielle
entreprise . catégorie d'activité . service ou vente: service
entreprise . catégorie d'activité . restauration ou hébergement: true
entreprise . catégorie d'activité . restauration ou hébergement: oui

View File

@ -6,6 +6,7 @@
// renamed the test configuration may be adapted but the persisted snapshot will remain unchanged).
/* eslint-disable no-undef */
import rules from 'Publicode/rules'
import artisteAuteurConfig from '../../source/components/simulationConfigs/artiste-auteur.yaml'
import autoentrepreneurConfig from '../../source/components/simulationConfigs/auto-entrepreneur.yaml'
import independantConfig from '../../source/components/simulationConfigs/indépendant.yaml'
@ -19,7 +20,7 @@ import remunerationDirigeantSituations from './simulations-rémunération-dirige
import employeeSituations from './simulations-salarié.yaml'
const roundResult = arr => arr.map(x => Math.round(x))
const engine = new Engine()
const engine = new Engine({ rules })
const runSimulations = (
situations,
targets,

40
test/ruleUtils.test.js Normal file
View File

@ -0,0 +1,40 @@
import { expect } from 'chai'
import { disambiguateRuleReference, ruleParents } from 'Engine/ruleUtils'
describe('ruleParents', function() {
it('should procude an array of the parents of a rule', function() {
let parents = ruleParents(
'CDD . taxe . montant annuel . exonération annuelle'
)
expect(parents).to.eql(['CDD . taxe . montant annuel', 'CDD . taxe', 'CDD'])
})
})
describe('disambiguateRuleReference', function() {
it("should disambiguate a reference to another rule in a rule, given the latter's namespace", function() {
const rawRules = {
CDD: { question: 'CDD ?' },
'CDD . taxe': { formule: 'montant annuel / 12' },
'CDD . condition': {},
'CDD . taxe . montant annuel': {
formule: '20 - exonération annuelle'
},
'CDD . taxe . montant annuel . exonération annuelle': {
formule: 20
}
}
expect(
disambiguateRuleReference(
rawRules,
'CDD . taxe . montant annuel',
'exonération annuelle'
)
).to.eql('CDD . taxe . montant annuel . exonération annuelle')
expect(
disambiguateRuleReference(
rawRules,
'CDD . taxe . montant annuel',
'condition'
)
).to.eql('CDD . condition')
})
})

View File

@ -1,140 +0,0 @@
import { expect } from 'chai'
import { map } from 'ramda'
import {
disambiguateRuleReference,
enrichRule,
nestedSituationToPathMap,
ruleParents,
rules,
translateAll
} from '../source/engine/rules'
describe('enrichRule', function() {
it('should extract the dotted name of the rule', function() {
let rule = { nom: 'contrat salarié . CDD' }
expect(enrichRule(rule)).to.have.property('name', 'CDD')
expect(enrichRule(rule)).to.have.property(
'dottedName',
'contrat salarié . CDD'
)
})
})
describe('rule checks', function() {
it('most input rules should have defaults', function() {
let rulesNeedingDefault = rules.filter(
r =>
r.espace &&
!r.simulateur &&
(!r.formule || r.formule['une possibilité']) &&
r.defaultValue == null &&
r.question &&
!['impôt . taux personnalisé'].includes(r.dottedName)
)
rulesNeedingDefault.map(r =>
//eslint-disable-next-line
console.log(
'La règle suivante doit avoir une valeur par défaut : ',
r.dottedName
)
)
expect(rulesNeedingDefault).to.be.empty
})
})
it('rules with a formula should not have defaults', function() {
let errors = rules.filter(
r =>
r.formule !== undefined &&
!r.formule['une possibilité'] &&
r.defaultValue !== undefined
)
// variant formulas are an exception, their implementation is to refactor TODO
expect(errors).to.be.empty
})
describe('translateAll', function() {
it('should translate flat rules', function() {
let rules = [
{
nom: 'foo . bar',
titre: 'Titre',
description: 'Description',
question: 'Question'
}
]
let translations = {
'foo . bar': {
'titre.en': 'TITRE',
'description.en': 'DESC',
'question.en': 'QUEST'
}
}
let result = translateAll(translations, map(enrichRule, rules))
expect(result[0]).to.have.property('titre', 'TITRE')
expect(result[0]).to.have.property('description', 'DESC')
expect(result[0]).to.have.property('question', 'QUEST')
})
})
describe('misc', function() {
it('should unnest nested form values', function() {
let values = {
'contrat salarié': { rémunération: { 'brut de base': '2300' } }
}
let pathMap = nestedSituationToPathMap(values)
expect(pathMap).to.have.property(
'contrat salarié . rémunération . brut de base',
'2300'
)
})
it('should procude an array of the parents of a rule', function() {
let rawRules = [
{ nom: 'CDD', question: 'CDD ?' },
{ nom: 'CDD . taxe', formule: 'montant annuel / 12' },
{
nom: 'CDD . taxe . montant annuel',
formule: '20 - exonération annuelle'
},
{
nom: 'CDD . taxe . montant annuel . exonération annuelle',
formule: 20
}
]
let parents = ruleParents(rawRules.map(enrichRule)[3].dottedName)
expect(parents).to.eql([
['CDD', 'taxe', 'montant annuel'],
['CDD', 'taxe'],
['CDD']
])
})
it("should disambiguate a reference to another rule in a rule, given the latter's namespace", function() {
let rawRules = [
{ nom: 'CDD', question: 'CDD ?' },
{ nom: 'CDD . taxe', formule: 'montant annuel / 12' },
{
nom: 'CDD . taxe . montant annuel',
formule: '20 - exonération annuelle'
},
{
nom: 'CDD . taxe . montant annuel . exonération annuelle',
formule: 20
}
]
let enrichedRules = rawRules.map(enrichRule),
resolved = disambiguateRuleReference(
enrichedRules,
enrichedRules[2],
'exonération annuelle'
)
expect(resolved).to.eql(
'CDD . taxe . montant annuel . exonération annuelle'
)
})
})

View File

@ -15,10 +15,8 @@ douche . nombre:
douche . impact par douche:
formule: impact par litre * litres d'eau par douche
douche . impact par litre:
formule: eau . impact par litre froid + chauffage . impact par litre
douche . litres d'eau par douche:
icônes: 🇱
formule: durée de la douche * litres par minute / 1 douche
@ -94,7 +92,6 @@ chauffage . énergie consommée par litre:
chauffage . impact par litre:
formule: impact par kWh * énergie consommée par litre
# Meilleure syntaxe : nouveau mécanisme correspondance
# mais où désigne-t-on ce sur quoi la correspondance se fait ? Est-ce implicite ? Ici le chauffage.
# formule:

View File

@ -27,17 +27,11 @@ impôt sur les sociétés:
références:
fiche service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23575
bénéfice:
formule: chiffre affaires - salaire total
bénéfice: chiffre affaires - salaire total
dividendes:
dividendes . brut:
formule: bénéfice - impôt sur les sociétés
dividendes . net:
formule: brut - prélèvement forfaitaire unique
dividendes . brut: bénéfice - impôt sur les sociétés
dividendes . net: brut - prélèvement forfaitaire unique
dividendes . prélèvement forfaitaire unique:
formule:
produit:
@ -46,8 +40,6 @@ dividendes . prélèvement forfaitaire unique:
- taux: 17.2%
- taux: 12.8%
salaire total:
formule: chiffre affaires * répartition salaire sur dividendes
salaire total: chiffre affaires * répartition salaire sur dividendes
revenu net après impôt:
formule: contrat salarié . rémunération . net après impôt + dividendes . net

View File

@ -1,393 +0,0 @@
import { expect } from 'chai'
import { enrichRule } from '../source/engine/rules'
import { analyse, parseAll } from '../source/engine/traverse'
let stateSelector = () => undefined
describe('analyse', function() {
it('should directly return simple numerical values', function() {
let rule = { nom: 'startHere', formule: 3269 }
let rules = parseAll([rule].map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 3269)
})
it('should compute expressions combining constants', function() {
let rule = { nom: 'startHere', formule: '32 + 69' }
let rules = parseAll([rule].map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 101)
})
})
describe('analyse on raw rules', function() {
it('should handle direct referencing of a variable', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: 'dix' },
{ nom: 'top . dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 10)
})
it('should handle expressions referencing other rules', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: '3259 + dix' },
{ nom: 'top . dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 3269)
})
it('should handle applicability conditions', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: '3259 + dix' },
{ nom: 'top . dix', formule: 10, 'non applicable si': 'vrai' },
{ nom: 'top . vrai', formule: '2 > 1' }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 3259)
})
it('should handle comparisons', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: '3259 > dix' },
{ nom: 'top . dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', true)
})
/* TODO: make this pass
it('should handle applicability conditions', function() {
let rawRules = [
{nom: "startHere", formule: "3259 + dix", espace: "top"},
{nom: "dix", formule: 10, espace: "top", "non applicable si" : "vrai"},
{nom: "vrai", formule: "1", espace: "top"}],
rules = rawRules.map(enrichRule)
expect(analyse(rules,"startHere")(stateSelector).targets[0]).to.have.property('nodeValue',3259)
});
*/
})
describe('analyse with mecanisms', function() {
it('should handle n-way "or"', function() {
let rawRules = [
{
nom: 'startHere',
formule: { 'une de ces conditions': ['1 > 2', '1 > 0', '0 > 2'] }
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', true)
})
it('should handle n-way "and"', function() {
let rawRules = [
{
nom: 'startHere',
formule: { 'toutes ces conditions': ['1 > 2', '1 > 0', '0 > 2'] }
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', false)
})
it('should handle percentages', function() {
let rawRules = [{ nom: 'top' }, { nom: 'top . startHere', formule: '35%' }],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 0.35)
})
it('should handle sums', function() {
let rawRules = [
{ nom: 'startHere', formule: { somme: [3200, 'dix', 9] } },
{ nom: 'dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 3219)
})
it('should handle multiplications', function() {
let rawRules = [
{
nom: 'startHere',
formule: {
produit: {
assiette: 3259,
plafond: 3200,
facteur: 1,
taux: 1.5
}
}
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 4800)
})
it('should handle components in multiplication', function() {
let rawRules = [
{
nom: 'startHere',
formule: {
produit: {
assiette: 3200,
composantes: [{ taux: 0.7 }, { taux: 0.8 }]
}
}
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 4800)
})
it('should apply a ceiling to the sum of components', function() {
let rawRules = [
{
nom: 'startHere',
formule: {
produit: {
assiette: 3259,
plafond: 3200,
composantes: [{ taux: 0.7 }, { taux: 0.8 }]
}
}
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 4800)
})
it('should handle progressive scales', function() {
let rawRules = [
{
nom: 'startHere',
formule: {
barème: {
assiette: 2008,
multiplicateur: 1000,
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 1.2 },
{ taux: 10 }
]
}
}
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 100 + 1200 + 80)
})
it('should handle progressive scales with components', function() {
let rawRules = [
{
nom: 'startHere',
formule: {
barème: {
assiette: 2008,
multiplicateur: 1000,
composantes: [
{
tranches: [
{ plafond: 1, taux: 0.05 },
{ plafond: 2, taux: 0.4 },
{ taux: 5 }
]
},
{
tranches: [
{ plafond: 1, taux: 0.05 },
{ plafond: 2, taux: 0.8 },
{ taux: 5 }
]
}
]
}
}
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 100 + 1200 + 80)
})
it('should handle progressive scales with variations', function() {
let rawRules = [
{
nom: 'startHere',
formule: {
barème: {
assiette: 2008,
multiplicateur: 1000,
variations: [
{
si: '3 > 4',
alors: {
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 1.2 },
{ taux: 10 }
]
}
},
{
si: '3 > 2',
alors: {
tranches: [
{ plafond: 1, taux: 0.1 },
{ plafond: 2, taux: 1.8 },
{ taux: 10 }
]
}
}
]
}
}
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 100 + 1800 + 80)
})
it('should handle max', function() {
let rawRules = [
{ nom: 'startHere', formule: { 'le maximum de': [3200, 60, 9] } }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 3200)
})
it('should handle filtering on components', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: 'composed .salarié' },
{
nom: 'top . composed',
formule: {
barème: {
assiette: 2008,
multiplicateur: 1000,
composantes: [
{
tranches: [
{ plafond: 1, taux: 0.05 },
{ plafond: 2, taux: 0.4 },
{ taux: 5 }
],
attributs: { 'dû par': 'salarié' }
},
{
tranches: [
{ plafond: 1, taux: 0.05 },
{ plafond: 2, taux: 0.8 },
{ taux: 5 }
],
attributs: { 'dû par': 'employeur' }
}
]
}
}
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 50 + 400 + 40)
})
it('should compute consistent values', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'top . startHere',
formule: 'composed .salarié + composed .employeur'
},
{ nom: 'top . orHere', formule: 'composed' },
{
nom: 'top . composed',
formule: {
barème: {
assiette: 2008,
multiplicateur: 1000,
composantes: [
{
tranches: [
{ plafond: 1, taux: 0.05 },
{ plafond: 2, taux: 0.4 },
{ taux: 5 }
],
attributs: { 'dû par': 'salarié' }
},
{
tranches: [
{ plafond: 1, taux: 0.05 },
{ plafond: 2, taux: 0.8 },
{ taux: 5 }
],
attributs: { 'dû par': 'employeur' }
}
]
}
}
}
],
rules = parseAll(rawRules.map(enrichRule))
expect(analyse(rules, 'orHere')(stateSelector).targets[0]).to.have.property(
'nodeValue',
100 + 1200 + 80
)
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 100 + 1200 + 80)
})
})
describe('Implicit parent applicability', function() {
it('should make a variable non applicable if one parent is input to false', function() {
let rawRules = [
{ nom: 'CDD', question: 'CDD ?' },
{ nom: 'CDD . surcoût', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'CDD . surcoût')(name => ({ CDD: false }[name])).targets[0]
).to.have.property('nodeValue', 0)
})
})

View File

@ -1,11 +1,11 @@
import { expect } from 'chai'
import { parseRules } from 'Engine'
import rawRules from 'Publicode/rules'
import { uniq } from 'ramda'
import { rulesFr } from '../source/engine/rules'
import { parseAll } from '../source/engine/traverse'
import unitsTranslations from '../source/locales/units.yaml'
it('has translation for all base units', () => {
const rules = parseAll(rulesFr)
const rules = parseRules(rawRules)
const units = uniq(
Object.keys(rules).reduce(
(prev, name) => [