⚙️ ajoute la conversion d'unité

Gros changements en perspective :
- Supprime la notion de période, au bénéfice de celle d'unité
  (`période : mensuelle` devient `unité: €/mois`)
- Améliore les rapports d'erreur avec des messages plus clair
- Ajoute un avertissement lorsque des types ne sont pas compatible
- Ajoute la conversion automatique d'unité dans le moteur
- Ajoute une notion d'unité par défaut de la simulation,
  c'est l'unité vers laquelle les règles qui ne spécifient pas
  d'unité seront converties
- Ajoute une notion d'unité par défaut des règles, qui spécifie
  l'unité de la règle qui prévaut lorsque qu'il n'y a pas
  d'unité par défaut de la simulation (utile pour les question ou
  pour s'assurer du bon type d'une règle)
pull/797/head
Johan Girod 2019-11-28 12:03:23 +01:00
parent 6b7f50fe4a
commit 00b122fa97
91 changed files with 2212 additions and 2372 deletions

View File

@ -1,6 +1,6 @@
{
"editor.formatOnSave": true,
"spellright.language": ["fr"],
"spellright.language": ["fr", "en"],
"spellright.documentTypes": ["yaml", "git-commit"],
"editor.codeActionsOnSave": {
"source.organizeImports": true

View File

@ -1,70 +1,86 @@
const fr = Cypress.env('language') === 'fr'
const inputSelector = 'input.currencyInput__input:not([name$="charges"])'
describe('Simulateurs', function () {
if (!fr) { return }
['indépendant', 'assimilé-salarié', 'auto-entrepreneur', 'salarié'].forEach(simulateur =>
describe(simulateur, () => {
before(() => cy.visit(`/simulateurs/${simulateur}`))
it('should not crash', function () {
cy.get(inputSelector)
})
describe('Simulateurs', function() {
if (!fr) {
return
}
;['indépendant', 'assimilé-salarié', 'auto-entrepreneur', 'salarié'].forEach(
simulateur =>
describe(simulateur, () => {
before(() => cy.visit(`/simulateurs/${simulateur}`))
it('should not crash', function() {
cy.get(inputSelector)
})
it('should display a result when entering a value in any of the currency input', () => {
cy.contains('année').click()
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
cy.get('input.currencyInput__input[name$="charges"]').type(1000)
}
cy.get(inputSelector).each((testedInput, i) => {
cy.wrap(testedInput).type('{selectall}60000')
cy.wait(600)
cy.contains('Cotisations')
cy.get(inputSelector).each(($input, j) => {
const val = $input.val().replace(/[\s,.]/g, '')
if (i != j) {
expect(val).not.to.be.eq('60000')
}
expect(val).to.match(/[1-9][\d]*$/)
})
})
})
it('should display a result when entering a value in any of the currency input', () => {
cy.contains('€/an').click()
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
cy.get('input.currencyInput__input[name$="charges"]').type(1000)
}
cy.get(inputSelector).each((testedInput, i) => {
cy.wrap(testedInput).type('{selectall}60000')
cy.wait(600)
cy.contains('Cotisations')
cy.get(inputSelector).each(($input, j) => {
const val = $input.val().replace(/[\s,.]/g, '')
if (i != j) {
expect(val).not.to.be.eq('60000')
}
expect(val).to.match(/[1-9][\d]*$/)
})
})
})
it('should allow to change period', function () {
cy.contains('année').click()
cy.wait(200)
cy.get(inputSelector).first().type('{selectall}12000')
cy.wait(600)
cy.contains('mois').click()
cy.get(inputSelector).first().invoke('val').should('match', /1[\s]000/)
})
it('should allow to change period', function() {
cy.contains('€/an').click()
cy.wait(200)
cy.get(inputSelector)
.first()
.type('{selectall}12000')
cy.wait(600)
cy.contains('€/mois').click()
cy.get(inputSelector)
.first()
.invoke('val')
.should('match', /1[\s]000/)
})
it('should allow to navigate to a documentation page', function () {
cy.get(inputSelector).first().type('{selectall}2000')
cy.wait(700)
cy.contains('Cotisations').click()
cy.location().should((loc) => {
expect(loc.pathname).to.match(/\/documentation\/.*\/cotisations/)
})
})
it('should allow to navigate to a documentation page', function() {
cy.get(inputSelector)
.first()
.type('{selectall}2000')
cy.wait(700)
cy.contains('Cotisations').click()
cy.location().should(loc => {
expect(loc.pathname).to.match(/\/documentation\/.*\/cotisations/)
})
})
it('should allow to go back to the simulation', function () {
cy.contains('← ').click();
cy.get(inputSelector).first().invoke('val').should('be', '2000')
})
it('should allow to go back to the simulation', function() {
cy.contains('← ').click()
cy.get(inputSelector)
.first()
.invoke('val')
.should('be', '2000')
})
if (simulateur === 'salarié') {
it('should save the current simulation', function () {
cy.get(inputSelector).first().type('{selectall}2137')
cy.contains('Passer').click()
cy.contains('Passer').click()
cy.contains('Passer').click()
cy.wait(1600)
cy.visit('/simulateurs/salarié')
cy.contains('Retrouver ma simulation').click()
cy.get(inputSelector).first().invoke('val').should('match', /2[\s]137/)
})
}
})
)
})
if (simulateur === 'salarié') {
it('should save the current simulation', function() {
cy.get(inputSelector)
.first()
.type('{selectall}2137')
cy.contains('Passer').click()
cy.contains('Passer').click()
cy.contains('Passer').click()
cy.wait(1600)
cy.visit('/simulateurs/salarié')
cy.contains('Retrouver ma simulation').click()
cy.get(inputSelector)
.first()
.invoke('val')
.should('match', /2[\s]137/)
})
}
})
)
})

View File

@ -1,6 +1,6 @@
import { SitePaths } from 'Components/utils/withSitePaths'
import { History } from 'history'
import { RootState } from 'Reducers/rootReducer'
import { RootState, SimulationConfig } from 'Reducers/rootReducer'
import { ThunkAction } from 'redux-thunk'
import { DottedName } from 'Types/rule'
import { deletePersistedSimulation } from '../storage/persistSimulation'
@ -13,10 +13,11 @@ export type Action =
| DeletePreviousSimulationAction
| SetExempleAction
| ExplainVariableAction
| UpdatePeriodAction
| UpdateSituationAction
| HideControlAction
| LoadPreviousSimulationAction
| SetSituationBranchAction
| UpdateDefaultUnit
| SetActiveTargetAction
type ThunkResult<R> = ThunkAction<
@ -35,7 +36,7 @@ type StepAction = {
type SetSimulationConfigAction = {
type: 'SET_SIMULATION'
url: string
config: Object
config: SimulationConfig
}
type DeletePreviousSimulationAction = {
@ -51,12 +52,13 @@ type SetExempleAction = {
type ResetSimulationAction = ReturnType<typeof resetSimulation>
type UpdateAction = ReturnType<typeof updateSituation>
type UpdatePeriodAction = ReturnType<typeof updatePeriod>
type UpdateSituationAction = ReturnType<typeof updateSituation>
type LoadPreviousSimulationAction = ReturnType<typeof loadPreviousSimulation>
type SetSituationBranchAction = ReturnType<typeof setSituationBranch>
type SetActiveTargetAction = ReturnType<typeof setActiveTarget>
type HideControlAction = ReturnType<typeof hideControl>
type ExplainVariableAction = ReturnType<typeof explainVariable>
type UpdateDefaultUnit = ReturnType<typeof updateUnit>
export const resetSimulation = () =>
({
@ -90,9 +92,12 @@ export const setSituationBranch = (id: number) =>
export const setSimulationConfig = (config: Object): ThunkResult<void> => (
dispatch,
_,
getState,
{ history }
): void => {
if (getState().simulation?.config === config) {
return
}
const url = history.location.pathname
dispatch({
type: 'SET_SIMULATION',
@ -121,10 +126,10 @@ export const updateSituation = (fieldName: DottedName, value: any) =>
value
} as const)
export const updatePeriod = (toPeriod: string) =>
export const updateUnit = (defaultUnit: string) =>
({
type: 'UPDATE_PERIOD',
toPeriod
type: 'UPDATE_DEFAULT_UNIT',
defaultUnit
} as const)
export function setExample(name: string, situation, dottedName: string) {

View File

@ -53,10 +53,15 @@ export default compose(
<div className="payslip__salarySection">
<Line
rule={getRule('contrat salarié . temps de travail')}
unit="heures/mois"
maximumFractionDigits={1}
/>
{heuresSupplémentaires?.nodeValue > 0 && (
<Line rule={heuresSupplémentaires} maximumFractionDigits={1} />
<Line
rule={heuresSupplémentaires}
unit="heures/mois"
maximumFractionDigits={1}
/>
)}
</div>

View File

@ -42,7 +42,7 @@ export let SalaireBrutSection = ({ getRule }) => {
export let Line = ({ rule, ...props }) => (
<>
<RuleLink {...rule} />
<Value {...rule} nilValueSymbol="—" {...props} />
<Value {...rule} nilValueSymbol="—" unit="€" {...props} />
</>
)

View File

@ -1,40 +1,28 @@
import { updatePeriod } from 'Actions/actions'
import { updateUnit } from 'Actions/actions'
import React from 'react'
import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { situationSelector } from 'Selectors/analyseSelectors'
import { defaultUnitsSelector } from 'Selectors/analyseSelectors'
import './PeriodSwitch.css'
export default function PeriodSwitch() {
const dispatch = useDispatch()
const situation = useSelector(situationSelector)
const defaultPeriod = useSelector(
(state: RootState) =>
state.simulation?.config?.situation?.période || 'année'
)
const currentPeriod = situation.période
let periods = ['année', 'mois']
const currentUnit = useSelector(defaultUnitsSelector)[0]
if (!currentPeriod) {
dispatch(updatePeriod(defaultPeriod))
}
let units = ['€/mois', '€/an']
return (
<span id="PeriodSwitch">
<span className="base ui__ small toggle">
{periods.map(period => (
<label key={period}>
{units.map(unit => (
<label key={unit}>
<input
name="période"
name="defaultUnit"
type="radio"
value={period}
onChange={() => dispatch(updatePeriod(period))}
checked={currentPeriod === period}
value={unit}
onChange={() => dispatch(updateUnit(unit))}
checked={currentUnit === unit}
/>
<span>
<Trans>{period}</Trans>
</span>
<span>{unit}</span>
</label>
))}
</span>

View File

@ -11,7 +11,7 @@ import { useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
usePeriod
defaultUnitsSelector
} from 'Selectors/analyseSelectors'
import * as Animate from 'Ui/animate'
@ -126,15 +126,15 @@ function RevenueRepatitionSection() {
}
function PaySlipSection() {
const period = usePeriod()
const unit = useSelector(defaultUnitsSelector)[0]
return (
<section>
<h2>
<Trans>
{period === 'mois'
? 'Fiche de paie mensuelle'
: 'Détail annuel des cotisations'}
</Trans>
{unit.endsWith('mois') ? (
<Trans>Fiche de paie</Trans>
) : (
<Trans>Détail annuel des cotisations</Trans>
)}
</h2>
<PaySlip />
</section>

View File

@ -1,4 +1,4 @@
import { setSituationBranch } from 'Actions/actions'
import { setSimulationConfig, setSituationBranch } from 'Actions/actions'
import {
defineDirectorStatus,
isAutoentrepreneur
@ -9,12 +9,11 @@ import Conversation from 'Components/conversation/Conversation'
import SeeAnswersButton from 'Components/conversation/SeeAnswersButton'
import PeriodSwitch from 'Components/PeriodSwitch'
import ComparaisonConfig from 'Components/simulationConfigs/rémunération-dirigeant.yaml'
import { useSimulationConfig } from 'Components/simulationConfigs/useSimulationConfig'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
import { encodeRuleName, getRuleFromAnalysis } from 'Engine/rules.js'
import revenusSVG from 'Images/revenus.svg'
import React, { useCallback, useContext, useState } from 'react'
import { default as React, useCallback, useContext, useState } from 'react'
import emoji from 'react-easy-emoji'
import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
@ -45,8 +44,9 @@ export default function SchemeComparaison({
hideAutoEntrepreneur = false,
hideAssimiléSalarié = false
}: SchemeComparaisonProps) {
useSimulationConfig(ComparaisonConfig)
const dispatch = useDispatch()
dispatch(setSimulationConfig(ComparaisonConfig))
const analyses = useSelector(analysisWithDefaultsSelector)
const plafondAutoEntrepreneurDépassé = useSelector((state: RootState) =>
branchAnalyseSelector(state, {
@ -298,7 +298,7 @@ export default function SchemeComparaison({
{conversationStarted && (
<>
<T k="comparaisonRégimes.période">
<h3 className="legend">Période</h3>
<h3 className="legend">Unité</h3>
</T>
<div className="AS-indep-et-auto" style={{ alignSelf: 'start' }}>
<PeriodSwitch />

View File

@ -94,7 +94,6 @@ export default function StackedBarChart({ data }: StackedBarChartProps) {
}))
const styles = useSpring({ opacity: displayChart ? 1 : 0 })
return (
<animated.div ref={intersectionRef} style={styles}>
<BarStack>

View File

@ -187,8 +187,7 @@ const Target = ({ target, initialRender }) => {
onFirstClick={value => {
dispatch(updateSituation(target.dottedName, value))
}}
rulePeriod={target.période}
colouredBackground={true}
unit={target.defaultUnit}
/>
</Animate.fromTop>
)}
@ -237,7 +236,7 @@ let TargetInputOrValue = ({
: undefined
const inversionFail = useSelector(
(state: RootState) =>
analysisWithDefaultsSelector(state)?.cache.inversionFail
analysisWithDefaultsSelector(state)?.cache._meta.inversionFail
)
const blurValue = inversionFail && !isActiveInput && value

View File

@ -72,7 +72,6 @@ export default function Value({
value: nodeValue
})
)
return nodeValue == undefined ? null : (
<span css={style(customCSS)} className="value">
{negative ? '-' : ''}

View File

@ -1,12 +1,9 @@
import classnames from 'classnames'
import { T } from 'Components'
import withColours from 'Components/utils/withColours'
import { currencyFormat } from 'Engine/format'
import { compose } from 'ramda'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import NumberFormat from 'react-number-format'
import { usePeriod } from 'Selectors/analyseSelectors'
import { debounce } from '../../utils'
import { FormDecorator } from './FormDecorator'
import InputSuggestions from './InputSuggestions'
@ -20,15 +17,12 @@ export default compose(
suggestions,
setFormValue,
submit,
rulePeriod,
dottedName,
value,
colours,
unit
}) {
const period = usePeriod()
const debouncedSetFormValue = useCallback(debounce(750, setFormValue), [])
const suffixed = unit != null && unit !== '%'
const { language } = useTranslation().i18n
const { thousandSeparator, decimalSeparator } = currencyFormat(language)
@ -42,44 +36,27 @@ export default compose(
setFormValue(value)
}}
onSecondClick={() => submit('suggestion')}
rulePeriod={rulePeriod}
/>
</div>
<div className="answer">
<NumberFormat
autoFocus
className={classnames({ suffixed })}
className={'suffixed'}
id={'step-' + dottedName}
thousandSeparator={thousandSeparator}
decimalSeparator={decimalSeparator}
suffix={unit === '%' ? ' %' : ''}
allowEmptyFormatting={true}
style={{ border: `1px solid ${colours.textColourOnWhite}` }}
onValueChange={({ floatValue }) => {
debouncedSetFormValue(unit === '%' ? floatValue / 100 : floatValue)
debouncedSetFormValue(floatValue)
}}
value={unit === '%' ? 100 * value : value}
value={value}
autoComplete="off"
/>
{suffixed && (
<label className="suffix" htmlFor={'step-' + dottedName}>
{unit}
{rulePeriod && rulePeriod !== 'aucune' && (
<span>
{' '}
<T>par</T>{' '}
<T>
{
{ mois: 'mois', année: 'an' }[
rulePeriod === 'flexible' ? period : rulePeriod
]
}
</T>
</span>
)}
</label>
)}
<label className="suffix" htmlFor={'step-' + dottedName}>
{unit}
</label>
<SendButton {...{ disabled: value === undefined, submit }} />
</div>
</>

View File

@ -1,39 +1,39 @@
import { toPairs } from 'ramda'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePeriod } from 'Selectors/analyseSelectors'
import { useSelector } from 'react-redux'
import { defaultUnitsSelector } from 'Selectors/analyseSelectors'
import { convertUnit, parseUnit } from '../../engine/units'
export default function InputSuggestions({
suggestions,
onSecondClick,
onSecondClick = x => x,
onFirstClick,
rulePeriod
unit
}) {
const [suggestion, setSuggestion] = useState(null)
const period = usePeriod()
const { t } = useTranslation()
const defaultUnit = parseUnit(useSelector(defaultUnitsSelector)[0])
if (!suggestions) return null
return (
<div css="display: flex; align-items: baseline; justify-content: flex-end;">
<small>Suggestions :</small>
{toPairs(suggestions).map(([text, value]) => {
// TODO : ce serait mieux de déplacer cette logique dans le moteur
const adjustedValue =
rulePeriod === 'flexible' && period === 'année' ? value * 12 : value
{toPairs(suggestions).map(([text, value]: [string, number]) => {
value = unit ? convertUnit(unit, defaultUnit, value) : value
return (
<button
className="ui__ link-button"
key={value}
css="margin: 0 0.4rem !important"
onClick={() => {
onFirstClick(adjustedValue)
if (suggestion !== adjustedValue) setSuggestion(adjustedValue)
else onSecondClick && onSecondClick(adjustedValue)
onFirstClick(value)
if (suggestion !== value) setSuggestion(value)
else onSecondClick && onSecondClick(value)
}}
title={t('cliquez pour insérer cette suggestion')}>
title={t('cliquez pour insérer cette suggestion')}
>
{text}
</button>
)

View File

@ -7,7 +7,7 @@ const worker = new Worker()
function SelectComponent({ setFormValue, submit, options }) {
const [searchResults, setSearchResults] = useState()
let submitOnChange = option => {
option.text = +option['Taux net'].replace(',', '.') / 100
option.text = +option['Taux net'].replace(',', '.')
setFormValue(option.text)
submit()
}

View File

@ -1,5 +1,4 @@
import { T } from 'Components'
import PeriodSwitch from 'Components/PeriodSwitch'
import withColours from 'Components/utils/withColours'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import Value from 'Components/Value'
@ -59,7 +58,6 @@ export default compose(
let { type, name, acronyme, title, description, question, icon } = flatRule,
namespaceRules = findRuleByNamespace(flatRules, dottedName)
let displayedRule = analysedExample || analysedRule
const renderToggleSourceButton = () => {
return (
<button
@ -144,17 +142,9 @@ export default compose(
>
<Value
{...displayedRule}
nilValueSymbol={
displayedRule.parentDependencies.some(
parent => parent?.nodeValue == false
)
? '-'
: null
}
/>
<Period
period={flatRule['période']}
valuesToShow={valuesToShow}
nilValueSymbol={displayedRule.parentDependencies.some(
parent => parent?.nodeValue == false
)}
/>
</div>
{displayedRule.defaultValue != null && (
@ -163,6 +153,7 @@ export default compose(
<Value
{...displayedRule}
nodeValue={displayedRule.defaultValue}
unit={displayedRule.unit || displayedRule.defaultUnit}
/>
</div>
)}
@ -257,20 +248,3 @@ let NamespaceRulesList = compose(withColours)(({ namespaceRules, colours }) => {
</section>
)
})
let Period = ({ period, valuesToShow }) =>
period ? (
valuesToShow && period === 'flexible' ? (
<PeriodSwitch />
) : (
<span className="inlineMecanism">
<span
className="name"
data-term-definition="période"
style={{ background: '#8e44ad' }}
>
{period}
</span>
</span>
)
) : null

View File

@ -34,7 +34,7 @@ questions:
- contrat salarié . complémentaire santé . part employeur
- contrat salarié . régime des impatriés
unités par défaut: [€/an]
situation:
dirigeant: 'assimilé salarié'
contrat salarié . ATMP . taux réduit: oui
période: année

View File

@ -16,6 +16,6 @@ questions:
liste noire:
- entreprise . charges
unités par défaut: [€/an]
situation:
dirigeant: 'auto-entrepreneur'
période: année

View File

@ -22,6 +22,6 @@ questions:
liste noire:
- entreprise . charges
unités par défaut: [€/an]
situation:
dirigeant: 'indépendant'
période: année

View File

@ -18,9 +18,7 @@ questions:
- entreprise . catégorie d'activité . restauration ou hébergement
- entreprise . catégorie d'activité . libérale règlementée
situation:
période: année
unités par défaut: [€/an]
branches:
- nom: Assimilé salarié
situation:

View File

@ -28,6 +28,6 @@ questions:
- contrat salarié . statut JEI
- contrat salarié . complémentaire santé . part employeur
- contrat salarié . régime des impatriés
unités par défaut: [€/mois]
situation:
dirigeant: non
période: mois

View File

@ -1,16 +0,0 @@
import { resetSimulation, setSimulationConfig } from 'Actions/actions'
import { useDispatch, useSelector } from 'react-redux'
import { RootState, SimulationConfig } from 'Reducers/rootReducer'
export function useSimulationConfig(config: SimulationConfig) {
const dispatch = useDispatch()
const stateConfig = useSelector(
(state: RootState) => state.simulation?.config
)
if (config !== stateConfig) {
dispatch(setSimulationConfig(config))
if (stateConfig) {
dispatch(resetSimulation())
}
}
}

View File

@ -2,7 +2,8 @@ import { formatCurrency } from 'Engine/format'
import React, { useRef } from 'react'
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
import { useTranslation } from 'react-i18next'
import { usePeriod } from 'Selectors/analyseSelectors'
import { useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import './AnimatedTargetValue.css'
type AnimatedTargetValueProps = {
@ -22,9 +23,11 @@ export default function AnimatedTargetValue({
const previousValue = useRef<number>()
const { language } = useTranslation().i18n
// We don't want to show the animated if the difference comes from a change in the period
const currentPeriod = usePeriod()
const previousPeriod = useRef(currentPeriod)
// We don't want to show the animated if the difference comes from a change in the unit
const currentUnit = useSelector(
(state: RootState) => state.simulation.defaultUnits[0]
)
const previousUnit = useRef(currentUnit)
const difference =
previousValue.current === value || Number.isNaN(value)
@ -32,11 +35,11 @@ export default function AnimatedTargetValue({
: (value || 0) - (previousValue.current || 0)
const shouldDisplayDifference =
difference !== null &&
previousPeriod.current === currentPeriod &&
previousUnit.current === currentUnit &&
Math.abs(difference) > 1
previousValue.current = value
previousPeriod.current = currentPeriod
previousUnit.current = currentUnit
return (
<>

View File

@ -47,20 +47,3 @@ formule:
2017: 6%
2016: 2%
```
Celle-ci est similaire à `variations` mais ne contient pas de `si` et est donc plus brève.
On peut la voir comme une alternative adaptée à certains endroits (?).
```yaml
formule:
assiette: assiette cotisations sociales
taux:
aiguillage numérique:
statut cadre = non:
2017: 16%
2016: 12%
statut cadre = oui:
2017: 6%
2016: 2%
```

View File

@ -1,18 +0,0 @@
- multiplication:
assiette:
transformation:
période: annuelle
variable: salaire de base
taux: 10%
- multiplication:
assiette:
période: annuelle
variable: salaire de base
taux: 10%
# Ce dernier est surement le plus lisible
# Mais ne permet pas un système générique de transformation
- multiplication:
assiette: salaire de base [annuel]
taux: 10%

View File

@ -10,7 +10,7 @@ export let evaluateControls = (cache, situationGate, parsedRules) =>
getControls(rule).map(control => ({
...control,
evaluated: evaluateNode(
cache,
{ ...cache, contextRule: [rule.dottedName] },
situationGate,
parsedRules,
control.testExpression

38
source/engine/error.ts Normal file
View File

@ -0,0 +1,38 @@
import { coerceArray } from '../utils'
export function syntaxError(
dottedName: string,
message: string,
originalError: Error
) {
throw new Error(
`[ Erreur syntaxique ]
Dans la règle \`${dottedName}\`,
${message}
${originalError && originalError.message}
`
)
}
export function typeWarning(
rules: string[] | string,
message: string,
originalError?: Error
) {
console.warn(
`[ Erreur de type ]
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`,
${message}
${originalError && originalError.message}
`
)
}
export function warning(dottedName: string, message: string, solution: string) {
console.warn(
`[ Avertissement ]
Dans la règle \`${dottedName}\`,
${message}
💡${solution}
`
)
}

View File

@ -1,6 +1,9 @@
import { bonus, evaluateNode, mergeMissing } from 'Engine/evaluation'
import { map, mergeAll, pick, pipe } from 'ramda'
import { typeWarning } from './error'
import { convertNodeToUnit } from './nodeUnits'
import { anyNull, undefOrTruthy, val } from './traverse-common-functions'
import { areUnitConvertible } from './units'
export const evaluateApplicability = (
cache,
@ -46,7 +49,8 @@ export const evaluateApplicability = (
}
export default (cache, situationGate, parsedRules, node) => {
cache.parseLevel++
cache._meta.parseLevel++
cache._meta.contextRule.push(node.dottedName)
let applicabilityEvaluation = evaluateApplicability(
cache,
situationGate,
@ -80,15 +84,35 @@ export default (cache, situationGate, parsedRules, node) => {
bonus(condMissing, !!Object.keys(condMissing).length),
formulaMissingVariables
)
cache.parseLevel--
if (node.dottedName.startsWith('sum')) {
// console.log(node.dottedName, missingVariables, node)
const unit =
node.unit ||
(node.defaultUnit &&
cache._meta.defaultUnits.find(unit =>
areUnitConvertible(node.defaultUnit, unit)
)) ||
node.defaultUnit ||
evaluatedFormula.unit
if (unit) {
try {
nodeValue = convertNodeToUnit(unit, evaluatedFormula).nodeValue
} catch (e) {
typeWarning(
node.dottedName,
`L'unité de la règle est incompatible avec celle de sa formule`,
e
)
}
}
cache._meta.contextRule.pop()
cache._meta.parseLevel--
return {
...node,
...applicabilityEvaluation,
...{ formule: evaluatedFormula },
...(node.formule && { formule: evaluatedFormula }),
nodeValue,
unit,
isApplicable,
missingVariables
}

View File

@ -9,10 +9,12 @@ import {
keys,
map,
mergeWith,
pluck,
reduce,
values
} from 'ramda'
import React from 'react'
import { typeWarning } from './error'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
export let makeJsx = node =>
typeof node.jsx == 'function'
@ -28,8 +30,34 @@ export let mergeAllMissing = missings =>
export let mergeMissing = (left, right) =>
mergeWith(add, left || {}, right || {})
export let evaluateNode = (cache, situationGate, parsedRules, node) =>
node.evaluate ? node.evaluate(cache, situationGate, parsedRules, node) : node
export let evaluateNode = (cache, situationGate, parsedRules, node) => {
let evaluatedNode = node.evaluate
? node.evaluate(cache, situationGate, parsedRules, node)
: node
if (typeof evaluatedNode.nodeValue !== 'number') {
return evaluatedNode
}
evaluatedNode = node.unité
? convertNodeToUnit(node.unit, evaluatedNode)
: simplifyNodeUnit(evaluatedNode)
return evaluatedNode
}
const sameUnitValues = (explanation, contextRule, mecanismName) => {
const unit = explanation.map(n => n.unit).find(Boolean)
const values = explanation.map(node => {
try {
return convertNodeToUnit(unit, node).nodeValue
} catch (e) {
typeWarning(
contextRule,
`'${node.name}' a une unité incompatible avec celle du mécanisme ${mecanismName}`,
e
)
return node.nodeValue
}
})
return [unit, values]
}
export let evaluateArray = (reducer, start) => (
cache,
@ -40,13 +68,23 @@ export let evaluateArray = (reducer, start) => (
let evaluateOne = child =>
evaluateNode(cache, situationGate, parsedRules, child),
explanation = map(evaluateOne, node.explanation),
values = pluck('nodeValue', explanation),
nodeValue = any(equals(null), values)
[unit, values] = sameUnitValues(
explanation,
cache._meta.contextRule,
node.name
),
nodeValue = values.some(value => value === null)
? null
: reduce(reducer, start, values),
missingVariables =
node.nodeValue == null ? mergeAllMissing(explanation) : {}
return { ...node, nodeValue, explanation, missingVariables }
return {
...node,
nodeValue,
explanation,
missingVariables,
unit
}
}
export let evaluateArrayWithFilter = (evaluationFilter, reducer, start) => (
@ -61,14 +99,18 @@ export let evaluateArrayWithFilter = (evaluationFilter, reducer, start) => (
evaluateOne,
filter(evaluationFilter(situationGate), node.explanation)
),
values = pluck('nodeValue', explanation),
[unit, values] = sameUnitValues(
explanation,
cache._meta.contextRule,
node.name
),
nodeValue = any(equals(null), values)
? null
: reduce(reducer, start, values),
missingVariables =
node.nodeValue == null ? mergeAllMissing(explanation) : {}
return { ...node, nodeValue, explanation, missingVariables }
return { ...node, nodeValue, explanation, missingVariables, unit }
}
export let defaultNode = nodeValue => ({
@ -102,34 +144,17 @@ export let evaluateObject = (objectShape, effect) => (
let transforms = map(k => [k, evaluateOne], keys(objectShape)),
automaticExplanation = evolve(fromPairs(transforms))(node.explanation)
// the result of effect can either be just a nodeValue, or an object {additionalExplanation, nodeValue}. The latter is useful for a richer JSX visualisation of the mecanism : the view should not duplicate code to recompute intermediate values (e.g. for a marginal 'barème', the marginal 'tranche')
let evaluated = effect(automaticExplanation),
let evaluated = effect(automaticExplanation, cache),
explanation = is(Object, evaluated)
? { ...automaticExplanation, ...evaluated.additionalExplanation }
: automaticExplanation,
nodeValue = is(Object, evaluated) ? evaluated.nodeValue : evaluated,
missingVariables = mergeAllMissing(values(explanation))
return { ...node, nodeValue, explanation, missingVariables }
}
export let E = (cache, situationGate, parsedRules) => {
let missingVariables = {}
let valNode = element =>
evaluateNode(cache, situationGate, parsedRules, element)
let val = element => {
let evaluated = valNode(element)
// automatically add missing variables when a variable is evaluated and thus needed in this mecanism's evaluation
missingVariables = mergeMissing(
missingVariables,
evaluated.missingVariables
)
return evaluated.nodeValue
}
return {
val,
valNode,
missingVariables: () => missingVariables
}
return simplifyNodeUnit({
...node,
nodeValue,
explanation,
missingVariables,
unit: explanation.unit
})
}

View File

@ -1,6 +1,6 @@
import { expect } from 'chai'
import { formatCurrency, formatPercentage, formatValue } from './format'
import { parseUnit } from 'Engine/units'
import { formatCurrency, formatPercentage, formatValue } from './format'
describe('format engine values', () => {
it('format currencies', () => {
@ -12,9 +12,9 @@ describe('format engine values', () => {
})
it('format percentages', () => {
expect(formatPercentage(0.1)).to.equal('10%')
expect(formatPercentage(1)).to.equal('100%')
expect(formatPercentage(0.102)).to.equal('10.2%')
expect(formatPercentage(10)).to.equal('10%')
expect(formatPercentage(100)).to.equal('100%')
expect(formatPercentage(10.2)).to.equal('10.2%')
})
it('format values', () => {

View File

@ -87,7 +87,7 @@ export function formatValue({
style: 'percent',
maximumFractionDigits,
language
})(value)
})(value / 100)
default:
return (
numberFormatter({

View File

@ -33,7 +33,17 @@ export default rules => dottedName => {
return <SelectGéo {...{ ...commonProps }} />
if (rule.API) throw new Error("Le seul API implémenté est l'API géo")
if (rule.unit == null)
if (rule.suggestions == 'atmp-2017')
return (
<SelectAtmp
{...{
...commonProps,
suggestions: rule.suggestions
}}
/>
)
if (rule.unit == null && rule.defaultUnit == null)
return (
<Question
{...{
@ -46,25 +56,14 @@ export default rules => dottedName => {
/>
)
if (rule.suggestions == 'atmp-2017')
return (
<SelectAtmp
{...{
...commonProps,
suggestions: rule.suggestions
}}
/>
)
// Now the numeric input case
return (
<Input
{...{
...commonProps,
unit: serialiseUnit(rule.unit),
suggestions: rule.suggestions,
rulePeriod: rule.période
unit: serialiseUnit(rule.unit || rule.defaultUnit),
suggestions: rule.suggestions
}}
/>
)

View File

@ -6,21 +6,19 @@
# @preprocessor esmodule
@{%
const {string, filteredVariable, date, variable, temporalVariable, binaryOperation, unaryOperation, boolean, number, numberWithUnit, percentage } = require('./grammarFunctions')
const {string, filteredVariable, date, variable, variableWithConversion, binaryOperation, unaryOperation, boolean, number, numberWithUnit } = require('./grammarFunctions')
const moo = require("moo");
const dateRegexp = /(?:(?:0?[1-9]|[12][0-9]|3[01])\/)?(?:0?[1-9]|1[012])\/\d{4}/
const letter = '[a-zA-Z\u00C0-\u017F]';
const letter = '[a-zA-Z\u00C0-\u017F€$%]';
const letterOrNumber = '[a-zA-Z\u00C0-\u017F0-9\']';
const word = `${letter}(?:[\-']?${letterOrNumber}+)*`;
const wordOrNumber = `(?:${word}|${letterOrNumber}+)`
const words = `${word}(?:[\\s]?${wordOrNumber}+)*`
const numberRegExp = '-?(?:[1-9][0-9]+|[0-9])(?:\\.[0-9]+)?';
const percentageRegExp = numberRegExp + '\\%'
const lexer = moo.compile({
date: dateRegexp,
percentage: new RegExp(percentageRegExp),
'(': '(',
')': ')',
'[': '[',
@ -31,8 +29,8 @@ const lexer = moo.compile({
string: /'[ \t\.'a-zA-Z\-\u00C0-\u017F0-9 ]+'/,
additionSubstraction: /[\+-]/,
multiplicationDivision: ['*','/'],
'€': '€',
dot: ' . ',
'.': '.',
letterOrNumber: new RegExp(letterOrNumber),
space: { match: /[\s]+/, lineBreaks: true }
});
@ -52,7 +50,7 @@ main ->
NumericTerminal ->
Variable {% id %}
| TemporalVariable {% id %}
| VariableWithUnitConversion {% id %}
| FilteredVariable {% id %}
| number {% id %}
@ -80,18 +78,19 @@ NonNumericTerminal ->
Variable -> %words (%dot %words {% ([,words]) => words %}):* {% variable %}
BaseUnit ->
%words {% id %}
| "€" {% id %}
UnitDenominator ->
(%space):? "/" %words {% join %}
UnitNumerator -> %words ("." %words):? {% flattenJoin %}
Unit -> BaseUnit ("/" BaseUnit {% join %}):? {% join %}
Unit -> UnitNumerator:? UnitDenominator:* {% flattenJoin %}
UnitConversion -> "[" Unit "]" {% ([,unit]) => unit %}
VariableWithUnitConversion ->
Variable %space UnitConversion {% variableWithConversion %}
# | FilteredVariable %space UnitConversion {% variableWithConversion %} TODO
Filter -> "[" %words "]" {% ([,filter]) => filter %}
Filter -> "." %words {% ([,filter]) => filter %}
FilteredVariable -> Variable %space Filter {% filteredVariable %}
TemporalTransform -> "[" ("mensuel" | "annuel" {% id %}) "]" {% ([,temporality]) => temporality %}
TemporalVariable -> Variable %space TemporalTransform {% temporalVariable %}
AdditionSubstraction ->
AdditionSubstraction %space %additionSubstraction %space MultiplicationDivision {% binaryOperation('calculation') %}
| MultiplicationDivision {% id %}
@ -107,7 +106,6 @@ boolean ->
number ->
%number {% number %}
| %number %space Unit {% numberWithUnit %}
| %percentage {% percentage %}
| %number (%space):? Unit {% numberWithUnit %}
string -> %string {% string %}

View File

@ -16,17 +16,12 @@ export let unaryOperation = operationType => ([operator, , A]) => ({
}
})
export let filteredVariable = (
[{ variable }, , { value: filter }],
l,
reject
) =>
['mensuel', 'annuel'].includes(filter)
? reject
: { filter: { filter, explanation: variable } }
export let filteredVariable = ([{ variable }, , { value: filter }]) => ({
filter: { filter, explanation: variable }
})
export let temporalVariable = ([{ variable }, , temporalTransform]) => ({
temporalTransform: { explanation: variable, temporalTransform }
export let variableWithConversion = ([{ variable }, , unit]) => ({
unitConversion: { explanation: variable, unit: parseUnit(unit.value) }
})
export let variable = ([firstFragment, nextFragment], _, reject) => {
@ -54,15 +49,7 @@ export let numberWithUnit = ([number, , unit]) => ({
}
})
export let percentage = ([{ value }]) => ({
constant: {
type: 'percentage',
unit: parseUnit('%'),
nodeValue: parseFloat(value.slice(0, -1)) / 100
}
})
export let date = ([{ value }], ...otherstuf) => {
export let date = ([{ value }]) => {
let [jour, mois, année] = value.split('/')
if (!année) {
;[jour, mois, année] = ['01', jour, mois]

View File

@ -21,8 +21,28 @@ let enrichRules = input => {
return rulesList.map(enrichRule)
}
class Engine {
situation = {}
parsedRules
constructor(rules = rulesFr) {
this.parsedRules = parseAll(rules)
this.defaultValues = collectDefaults(rules)
}
evaluate(targets, { defaultUnits, situation }) {
this.evaluation = analyseMany(
this.parsedRules,
targets,
defaultUnits
)(dottedName => situation[dottedName] || this.defaultValues[dottedName])
return this.evaluation.targets.map(({ nodeValue }) => nodeValue)
}
getLastEvaluationExplanations() {
return this.evaluation
}
}
export default {
evaluate: (targetInput, input, config) => {
evaluate: (targetInput, input, config, defaultUnits = []) => {
let rules = config
? [
...(config.base ? enrichRules(config.base) : rulesFr),
@ -32,12 +52,14 @@ export default {
let evaluation = analyseMany(
parseAll(rules),
Array.isArray(targetInput) ? targetInput : [targetInput]
Array.isArray(targetInput) ? targetInput : [targetInput],
defaultUnits
)(inputToStateSelector(rules)(input))
if (config?.debug) return evaluation
let values = evaluation.targets.map(t => t.nodeValue)
return Array.isArray(targetInput) ? values : values[0]
}
},
Engine
}

View File

@ -30,25 +30,21 @@ toutes ces conditions:
Renvoie vrai si toutes les conditions vraies.
aiguillage numérique:
variations:
type: numeric
description: |
Contient une liste de couples condition-conséquence.
Couple par couple, si la condition est vraie, alors on choisit la conséquence.
Cette conséquence peut elle-même être un mécanisme `aiguillage numérique` ou plus simplement un `taux`.
Cette conséquence peut elle-même être un mécanisme `variations` ou plus simplement un `taux`.
Si aucune condition n'est vraie, alors ce mécanisme renvoie implicitement `non applicable` (ce qui peut se traduire par la valeur `0` si nous sommes dans un contexte numérique).
variations:
type: numeric
description: |
Contient une liste de couples condition-conséquence, sous une forme plus explicite que l'aiguillage numérique :
```
si: condition
alors: valeur
alors: consequence
```
Ce mécanisme peut aussi être utilisé au sein d'un mécanisme compatible, tel que la multiplication ou le barème. Par exemple, certains paramètres de la multiplication seront communs (ex. l'assiette) alors que d'autres (ex. le taux) variront selon une autre variable (ex. statut cadre).
@ -180,11 +176,3 @@ synchronisation:
description: |
Pour éviter trop de saisies à l'utilisateur, certaines informations sont récupérées à partir de ce que l'on appelle des API. Ce sont des services auxquels ont fait appel pour obtenir des informations sur un sujet précis. Par exemple, l'État français fournit gratuitement l'API géo, qui permet à partir du nom d'une ville, d'obtenir son code postal, son département, la population etc.
Ce mécanismes `synchronisation` permet de faire le lien entre les règles de notre système et les réponses de ces API.
période:
description: |
Une régle qui a une période `mois` ou `année`, c'est une règle qui ne peut être calculée que sur cette période. La période est `flexible` quand le calcul est valable quelle que soit la période choisie. D'autres règles ne changent pas de valeur en fonction de la période.
Par exemple, dans une simulation mensuelle, si `indemnité kilométrique vélo` (de période flexible) appelle la règle `distance annuelle`, qui est définie sur l'année, alors la valeur de cette dernière sera divisée par 12 avant d'être passée à cette première. L'inverse est également vrai, en multipliant par 12.
Par défaut, la période de la simulation est mensuelle.

View File

@ -3,23 +3,25 @@ import React from 'react'
import { makeJsx } from '../evaluation'
import { Node } from './common'
export default function Allègement(nodeValue, rawExplanation) {
export default function Allègement(nodeValue, rawExplanation, ...other) {
// properties with a nodeValue of 0 are not interesting to display
let explanation = map(
k => (k && k.nodeValue !== 0 ? k : null),
rawExplanation
)
console.log(other)
return (
<div>
<Node
classes="mecanism allègement"
name="allègement"
value={nodeValue}
unit={explanation.unit}
child={
<ul className="properties">
<li key="assiette">
<span className="key">assiette: </span>
<span className="value">{makeJsx(rawExplanation.assiette)}</span>
<span className="value">{makeJsx(explanation.assiette)}</span>
</li>
{explanation.franchise && (
<li key="franchise">
@ -41,6 +43,12 @@ export default function Allègement(nodeValue, rawExplanation) {
<span className="value">{makeJsx(explanation.abattement)}</span>
</li>
)}
{explanation.plafond && (
<li key="abattement">
<span className="key">plafond: </span>
<span className="value">{makeJsx(explanation.plafond)}</span>
</li>
)}
</ul>
}
/>

View File

@ -119,7 +119,10 @@ let Component = function Barème({
<Trans>Taux moyen</Trans> :{' '}
</b>
<NodeValuePointer
data={nodeValue / lazyEval(explanation['assiette']).nodeValue}
data={
(100 * nodeValue) /
lazyEval(explanation['assiette']).nodeValue
}
unit="%"
/>
</>
@ -142,6 +145,7 @@ let Tranche = ({
de: min,
à: max,
taux,
nodeValue,
montant
},
multiplicateur,
@ -186,7 +190,7 @@ let Tranche = ({
<td key="taux"> {taux != null ? makeJsx(taux) : montant}</td>
{showValues && !returnRate && taux != null && (
<td key="value">
<NodeValuePointer data={trancheValue} unit={resultUnit} />
<NodeValuePointer data={nodeValue} unit={resultUnit} />
</td>
)}
</tr>
@ -212,7 +216,8 @@ function TrancheFormatter({
{value}&nbsp;
<RuleLink
{...multiplicateur.explanation}
title={multiplicateur.explanation.name}>
title={multiplicateur.explanation.name}
>
{multiplicateurAcronym}
</RuleLink>{' '}
<NodeValuePointer

View File

@ -36,7 +36,8 @@ function Row({ v, i, unit }) {
className="mecanism-somme__row"
key={v.name || i}
// className={isSomme ? '' : 'noNest'}
onClick={() => setFolded(!folded)}>
onClick={() => setFolded(!folded)}
>
<div className="element">
{makeJsx(v)}
{isSomme && (
@ -46,13 +47,13 @@ function Row({ v, i, unit }) {
)}
</div>
<div className="situationValue value">
<NodeValuePointer data={v.nodeValue} unit={unit} />
<NodeValuePointer data={v.nodeValue} unit={v.unit} />
</div>
</div>,
...(isSomme && !folded
? [
<div className="nested" key={v.name + '-nest'}>
<Table explanation={rowFormula.explanation} unit={unit} />
<Table explanation={rowFormula.explanation} unit={v.unit || unit} />
</div>
]
: [])

View File

@ -1,26 +1,19 @@
import { decompose } from 'Engine/mecanisms/utils'
import variations from 'Engine/mecanisms/variations'
import { inferUnit } from 'Engine/units'
import { convertNodeToUnit } from 'Engine/nodeUnits'
import { inferUnit, isPercentUnit } from 'Engine/units'
import {
add,
any,
curry,
equals,
evolve,
filter,
find,
head,
is,
isEmpty,
keys,
map,
max,
mergeWith,
min,
path,
pipe,
pluck,
prop,
reduce,
subtract,
toPairs
@ -28,8 +21,8 @@ import {
import React from 'react'
import { Trans } from 'react-i18next'
import 'react-virtualized/styles.css'
import { typeWarning } from './error'
import {
bonus,
collectNodeMissing,
defaultNode,
evaluateArray,
@ -37,7 +30,6 @@ import {
evaluateObject,
makeJsx,
mergeAllMissing,
mergeMissing,
parseObject
} from './evaluation'
import Allègement from './mecanismViews/Allègement'
@ -48,6 +40,7 @@ import Somme from './mecanismViews/Somme'
import { disambiguateRuleReference, findRuleByDottedName } from './rules'
import { anyNull, val } from './traverse-common-functions'
import uniroot from './uniroot'
import { parseUnit } from './units'
export let mecanismOneOf = (recurse, k, v) => {
if (!is(Array, v)) throw new Error('should be array')
@ -145,117 +138,6 @@ export let mecanismAllOf = (recurse, k, v) => {
}
}
export let mecanismNumericalSwitch = (recurse, k, v) => {
// Si "l'aiguillage" est une constante ou une référence directe à une variable;
// l'utilité de ce cas correspond à un appel récursif au mécanisme
if (is(String, v)) return recurse(v)
if (!is(Object, v) || keys(v).length == 0) {
throw new Error(
'Le mécanisme "aiguillage numérique" et ses sous-logiques doivent contenir au moins une proposition'
)
}
// les termes sont les couples (condition, conséquence) de l'aiguillage numérique
let terms = toPairs(v)
// la conséquence peut être un 'string' ou un autre aiguillage numérique
let parseCondition = ([condition, consequence]) => {
let conditionNode = recurse(condition), // can be a 'comparison', a 'variable'
consequenceNode = mecanismNumericalSwitch(recurse, condition, consequence)
let evaluate = (cache, situationGate, parsedRules, node) => {
let explanation = evolve(
{
condition: curry(evaluateNode)(cache, situationGate, parsedRules),
consequence: curry(evaluateNode)(cache, situationGate, parsedRules)
},
node.explanation
),
leftMissing = explanation.condition.missingVariables,
investigate = explanation.condition.nodeValue !== false,
rightMissing = investigate
? explanation.consequence.missingVariables
: {},
missingVariables = mergeMissing(bonus(leftMissing), rightMissing)
return {
...node,
explanation,
missingVariables,
nodeValue: explanation.consequence.nodeValue,
condValue: explanation.condition.nodeValue
}
}
let jsx = (nodeValue, { condition, consequence }) => (
<div className="condition">
{makeJsx(condition)}
<div>{makeJsx(consequence)}</div>
</div>
)
return {
evaluate,
jsx,
explanation: { condition: conditionNode, consequence: consequenceNode },
category: 'condition',
text: condition,
condition: conditionNode,
type: 'boolean'
}
}
let evaluateTerms = (cache, situationGate, parsedRules, node) => {
let evaluateOne = child =>
evaluateNode(cache, situationGate, parsedRules, child),
explanation = map(evaluateOne, node.explanation),
nonFalsyTerms = filter(node => node.condValue !== false, explanation),
getFirst = o => pipe(head, prop(o))(nonFalsyTerms),
nodeValue =
// voilà le "numérique" dans le nom de ce mécanisme : il renvoie zéro si aucune condition n'est vérifiée
isEmpty(nonFalsyTerms)
? 0
: // c'est un 'null', on renvoie null car des variables sont manquantes
getFirst('condValue') == null
? null
: // c'est un true, on renvoie la valeur de la conséquence
getFirst('nodeValue'),
choice = find(node => node.condValue, explanation),
missingVariables = choice
? choice.missingVariables
: mergeAllMissing(explanation)
return { ...node, nodeValue, explanation, missingVariables }
}
let explanation = map(parseCondition, terms)
let jsx = (nodeValue, explanation) => (
<Node
classes="mecanism numericalSwitch list"
name="aiguillage numérique"
value={nodeValue}
child={
<ul>
{explanation.map(item => (
<li key={item.name || item.text}>{makeJsx(item)}</li>
))}
</ul>
}
/>
)
return {
evaluate: evaluateTerms,
jsx,
explanation,
category: 'mecanism',
name: 'aiguillage numérique',
type: 'boolean || numeric' // lol !
}
}
export let findInversion = (situationGate, parsedRules, v, dottedName) => {
let inversions = v.avec
if (!inversions)
@ -304,7 +186,13 @@ let doInversion = (oldCache, situationGate, parsedRules, v, dottedName) => {
let inversionCache = {}
let fx = x => {
inversionCache = { parseLevel: oldCache.parseLevel + 1, op: '<' }
inversionCache = {
_meta: {
...oldCache._meta,
parseLevel: oldCache._meta.parseLevel + 1,
op: '<'
}
}
let v = evaluateNode(
inversionCache, // with an empty cache
n =>
@ -365,7 +253,7 @@ export let mecanismInversion = dottedName => (recurse, k, v) => {
missingVariables = inversion.missingVariables
if (nodeValue === undefined)
cache.inversionFail = {
cache._meta.inversionFail = {
given: inversion.inversedWith.rule.dottedName,
estimated: dottedName
}
@ -425,11 +313,31 @@ export let mecanismReduction = (recurse, k, v) => {
franchise: defaultNode(0)
}
let effect = ({ assiette, abattement, plafond, franchise, décote }) => {
let effect = (
{ assiette, abattement, plafond, franchise, décote },
cache
) => {
let v_assiette = val(assiette)
if (v_assiette == null) return null
if (assiette.unit) {
try {
franchise = convertNodeToUnit(assiette.unit, franchise)
plafond = convertNodeToUnit(assiette.unit, plafond)
if (!isPercentUnit(abattement.unit)) {
abattement = convertNodeToUnit(assiette.unit, abattement)
}
if (décote) {
décote.plafond = convertNodeToUnit(assiette.unit, décote.plafond)
décote.taux = convertNodeToUnit(parseUnit(''), décote.taux)
}
} catch (e) {
typeWarning(
cache._meta.contextRule,
"Impossible de convertir les unités de l'allègement entre elles",
e
)
}
}
let montantFranchiséDécoté =
val(franchise) && v_assiette < val(franchise)
? 0
@ -443,13 +351,12 @@ export let mecanismReduction = (recurse, k, v) => {
: max(0, (1 + taux) * v_assiette - taux * plafondDécote)
})()
: v_assiette
return abattement
const nodeValue = abattement
? val(abattement) == null
? montantFranchiséDécoté === 0
? 0
: null
: abattement.type === 'percentage'
: isPercentUnit(abattement.unit)
? max(
0,
montantFranchiséDécoté -
@ -457,6 +364,15 @@ export let mecanismReduction = (recurse, k, v) => {
)
: max(0, montantFranchiséDécoté - min(val(plafond), val(abattement)))
: montantFranchiséDécoté
return {
nodeValue,
additionalExplanation: {
unit: assiette.unit,
franchise,
plafond,
abattement
}
}
}
let base = parseObject(recurse, objectShape, v),
@ -494,20 +410,39 @@ export let mecanismProduct = (recurse, k, v) => {
facteur: defaultNode(1),
plafond: defaultNode(Infinity)
}
let effect = ({ assiette, taux, facteur, plafond }) => {
let effect = ({ assiette, taux, facteur, plafond }, cache) => {
if (assiette.unit) {
try {
plafond = convertNodeToUnit(assiette.unit, plafond)
} catch (e) {
typeWarning(
cache._meta.contextRule,
"Impossible de convertir l'unité du plafond de la multiplication dans celle de l'assiette",
e
)
}
}
let mult = (base, rate, facteur, plafond) =>
Math.min(base, plafond) * rate * facteur
const unit = inferUnit(
'*',
[assiette, taux, facteur].map(el => el.unit)
)
const nodeValue =
val(taux) === 0 ||
val(taux) === false ||
val(assiette) === 0 ||
val(facteur) === 0
? 0
: anyNull([taux, assiette, facteur, plafond])
? null
: mult(val(assiette), val(taux), val(facteur), val(plafond))
return {
nodeValue:
val(taux) === 0 ||
val(taux) === false ||
val(assiette) === 0 ||
val(facteur) === 0
? 0
: anyNull([taux, assiette, facteur, plafond])
? null
: mult(val(assiette), val(taux), val(facteur), val(plafond)),
additionalExplanation: { plafondActif: val(assiette) > val(plafond) }
nodeValue,
additionalExplanation: {
plafondActif: val(assiette) > val(plafond),
unit
}
}
}

View File

@ -1,9 +1,8 @@
import { defaultNode, evaluateObject } from 'Engine/evaluation'
import { defaultNode, evaluateObject, parseObject } from 'Engine/evaluation'
import BarèmeContinu from 'Engine/mecanismViews/BarèmeContinu'
import { val, anyNull } from 'Engine/traverse-common-functions'
import { anyNull, val } from 'Engine/traverse-common-functions'
import { parseUnit } from 'Engine/units'
import { parseObject } from 'Engine/evaluation'
import { reduce, toPairs, sort, aperture, pipe, reduced, last } from 'ramda'
import { aperture, last, pipe, reduce, reduced, sort, toPairs } from 'ramda'
export default (recurse, k, v) => {
let objectShape = {
@ -24,8 +23,8 @@ export default (recurse, k, v) => {
reduce((_, [[lowerLimit, lowerRate], [upperLimit, upperRate]]) => {
let x1 = val(multiplicateur) * lowerLimit,
x2 = val(multiplicateur) * upperLimit,
y1 = val(assiette) * val(recurse(lowerRate)),
y2 = val(assiette) * val(recurse(upperRate))
y1 = (val(assiette) * val(recurse(lowerRate))) / 100,
y2 = (val(assiette) * val(recurse(upperRate))) / 100
if (val(assiette) > x1 && val(assiette) <= x2) {
// Outside of these 2 limits, it's a linear function a * x + b
let a = (y2 - y1) / (x2 - x1),
@ -33,16 +32,16 @@ export default (recurse, k, v) => {
nodeValue = a * val(assiette) + b,
taux = nodeValue / val(assiette)
return reduced({
nodeValue: returnRate ? taux : nodeValue,
nodeValue: returnRate ? taux * 100 : nodeValue,
additionalExplanation: {
seuil: val(assiette) / val(multiplicateur),
taux
taux,
unit: returnRate ? parseUnit('%') : assiette.unit
}
})
}
}, 0)
)(points)
return result
}
let explanation = {

View File

@ -3,8 +3,8 @@ import { decompose } from 'Engine/mecanisms/utils'
import variations from 'Engine/mecanisms/variations'
import Barème from 'Engine/mecanismViews/Barème'
import { val } from 'Engine/traverse-common-functions'
import { desugarScale } from './barème'
import { parseUnit } from 'Engine/units'
import { desugarScale } from './barème'
/* on réécrit en une syntaxe plus bas niveau mais plus régulière les tranches :
`en-dessous de: 1`
@ -41,13 +41,24 @@ export default (recurse, k, v) => {
roundedAssiette >= val(multiplicateur) * min &&
roundedAssiette <= max * val(multiplicateur)
)
if (!matchedTranche) return 0
if (matchedTranche.taux)
return returnRate
let nodeValue
if (!matchedTranche) {
nodeValue = 0
} else if (matchedTranche.taux) {
nodeValue = returnRate
? matchedTranche.taux.nodeValue
: matchedTranche.taux.nodeValue * val(assiette)
return matchedTranche.montant
: (matchedTranche.taux.nodeValue / 100) * val(assiette)
} else {
nodeValue = matchedTranche.montant.nodeValue
}
return {
nodeValue,
additionalExplanation: {
unit: returnRate
? parseUnit('%')
: (v['unité'] && parseUnit(v['unité'])) || explanation.assiette.unit
}
}
}
let explanation = {
@ -55,8 +66,10 @@ export default (recurse, k, v) => {
returnRate,
tranches
},
evaluate = evaluateObject(objectShape, effect)
evaluate = evaluateObject(objectShape, effect),
unit = returnRate
? parseUnit('%')
: (v['unité'] && parseUnit(v['unité'])) || explanation.assiette.unit
return {
evaluate,
jsx: Barème('linéaire'),
@ -65,6 +78,6 @@ export default (recurse, k, v) => {
name: 'barème linéaire',
barème: 'en taux',
type: 'numeric',
unit: returnRate ? parseUnit('%') : v['unité'] || explanation.assiette.unit
unit
}
}

View File

@ -1,10 +1,11 @@
import { defaultNode, E } from 'Engine/evaluation'
import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation'
import { decompose } from 'Engine/mecanisms/utils'
import variations from 'Engine/mecanisms/variations'
import Barème from 'Engine/mecanismViews/Barème'
import { val } from 'Engine/traverse-common-functions'
import { inferUnit, parseUnit } from 'Engine/units'
import { evolve, has, pluck, sum } from 'ramda'
import { evolve, has } from 'ramda'
import { typeWarning } from '../error'
import { convertNodeToUnit } from '../nodeUnits'
import { parseUnit } from '../units'
export let desugarScale = recurse => tranches =>
tranches
@ -15,7 +16,7 @@ export let desugarScale = recurse => tranches =>
? { ...t, de: t['au-dessus de'], à: Infinity }
: t
)
.map(evolve({ taux: recurse }))
.map(evolve({ taux: recurse, montant: recurse }))
// This function was also used for marginal barèmes, but now only for linear ones
export let trancheValue = (assiette, multiplicateur) => ({
@ -24,10 +25,10 @@ export let trancheValue = (assiette, multiplicateur) => ({
taux,
montant
}) =>
Math.round(val(assiette)) >= min * val(multiplicateur) &&
(!max || Math.round(val(assiette)) <= max * val(multiplicateur))
Math.round(assiette.nodeValue) >= min * multiplicateur.nodeValue &&
(!max || Math.round(assiette.nodeValue) <= max * multiplicateur.nodeValue)
? taux != null
? val(assiette) * val(taux)
? assiette.nodeValue * taux.nodeValue
: montant
: 0
@ -52,22 +53,66 @@ export default (recurse, k, v) => {
}
let evaluate = (cache, situationGate, parsedRules, node) => {
let e = E(cache, situationGate, parsedRules)
let { assiette, multiplicateur } = node.explanation,
tranches = node.explanation.tranches.map(tranche => {
let { de: min, à: max, taux } = tranche
let value =
e.val(assiette) < min * e.val(multiplicateur)
? 0
: (Math.min(e.val(assiette), max * e.val(multiplicateur)) -
min * e.val(multiplicateur)) *
e.val(taux)
return { ...tranche, value }
}),
nodeValue = sum(pluck('value', tranches))
let { assiette, multiplicateur } = node.explanation
assiette = evaluateNode(cache, situationGate, parsedRules, assiette)
multiplicateur = evaluateNode(
cache,
situationGate,
parsedRules,
multiplicateur
)
try {
multiplicateur = convertNodeToUnit(assiette.unit, multiplicateur)
} catch (e) {
typeWarning(
cache._meta.contextRule,
`L'unité du multiplicateur du barème doit être compatible avec celle de son assiette`,
e
)
}
const tranches = node.explanation.tranches.map(tranche => {
let { de: min, à: max, taux } = tranche
if (
[assiette, multiplicateur].every(
({ nodeValue }) => nodeValue != null
) &&
assiette.nodeValue < min * multiplicateur.nodeValue
) {
return { ...tranche, nodeValue: 0 }
}
taux = convertNodeToUnit(
parseUnit(''),
evaluateNode(cache, situationGate, parsedRules, taux)
)
if (
[assiette, multiplicateur, taux].some(
({ nodeValue }) => nodeValue == null
)
) {
return {
...tranche,
nodeValue: null,
missingVariables: taux.missingVariables
}
}
return {
...tranche,
nodeValue:
(Math.min(assiette.nodeValue, max * multiplicateur.nodeValue) -
min * multiplicateur.nodeValue) *
taux.nodeValue
}
})
const nodeValue = tranches.reduce(
(value, { nodeValue }) => (nodeValue == null ? null : value + nodeValue),
0
)
const missingVariables = mergeAllMissing([
assiette,
multiplicateur,
...tranches
])
return {
...node,
nodeValue,
@ -75,8 +120,9 @@ export default (recurse, k, v) => {
...explanation,
tranches
},
missingVariables: e.missingVariables(),
lazyEval: e.valNode
missingVariables,
unit: assiette.unit,
lazyEval: node => evaluateNode(cache, situationGate, parsedRules, node)
}
}
@ -87,6 +133,6 @@ export default (recurse, k, v) => {
category: 'mecanism',
name: 'barème',
barème: 'marginal',
unit: inferUnit('*', [explanation.assiette.unit, parseUnit('%')])
unit: explanation.assiette.unit
}
}

View File

@ -1,3 +1,4 @@
import { typeWarning } from 'Engine/error'
import {
defaultNode,
evaluateNode,
@ -5,49 +6,43 @@ import {
parseObject
} from 'Engine/evaluation'
import { Node } from 'Engine/mecanismViews/common'
import { convertNodeToUnit } from 'Engine/nodeUnits'
import React from 'react'
import { val } from '../traverse-common-functions'
function MecanismEncadrement({ nodeValue, explanation }) {
function MecanismEncadrement({ nodeValue, explanation, unit }) {
return (
<Node
classes="mecanism encadrement"
name="encadrement"
value={nodeValue}
unit={explanation.unit}
unit={unit}
child={
<>
{makeJsx(explanation.valeur)}
<ul className="properties">
<p>
{!explanation.plancher.isDefault && (
<li key="plancher">
<span
style={
nodeValue === val(explanation.plancher)
? { background: 'yellow' }
: {}
}
>
<span className="key">Minimum :</span>
<span className="value">{makeJsx(explanation.plancher)}</span>
</span>
</li>
<span
css={
nodeValue === val(explanation.plancher) &&
'background: yellow'
}
>
<strong className="key">Minimum : </strong>
<span className="value">{makeJsx(explanation.plancher)}</span>
</span>
)}
{!explanation.plafond.isDefault && (
<li key="plafond">
<span
style={
nodeValue === val(explanation.plafond)
? { background: 'yellow' }
: {}
}
>
<span className="key">Plafonné à :</span>
<span className="value">{makeJsx(explanation.plafond)}</span>
</span>
</li>
<span
css={
nodeValue === val(explanation.plafond) && 'background: yellow'
}
>
<strong className="key">Plafonné à : </strong>
<span className="value">{makeJsx(explanation.plafond)}</span>
</span>
)}
</ul>
</p>
</>
}
/>
@ -61,26 +56,33 @@ const objectShape = {
}
const evaluate = (cache, situation, parsedRules, node) => {
const valeur = evaluateNode(
cache,
situation,
parsedRules,
node.explanation.valeur
)
const plafond = evaluateNode(
cache,
situation,
parsedRules,
node.explanation.plafond
)
const plancher = evaluateNode(
cache,
situation,
parsedRules,
node.explanation.plancher
)
let evaluateAttribute = evaluateNode.bind(null, cache, situation, parsedRules)
const valeur = evaluateAttribute(node.explanation.valeur)
let plafond = evaluateAttribute(node.explanation.plafond)
let plancher = evaluateAttribute(node.explanation.plancher)
if (valeur.unit) {
try {
plafond = convertNodeToUnit(valeur.unit, plafond)
plancher = convertNodeToUnit(valeur.unit, plancher)
} catch (e) {
typeWarning(
cache._meta.contextRule,
"Le plafond / plancher de l'encadrement a une unité incompatible avec celle de la valeur à encadrer",
e
)
}
}
const nodeValue = Math.max(val(plancher), Math.min(val(plafond), val(valeur)))
return { ...node, nodeValue }
return {
...node,
nodeValue,
unit: valeur.unit,
explanation: {
valeur,
plafond,
plancher
}
}
}
export default (recurse, k, v) => {
@ -89,8 +91,12 @@ export default (recurse, k, v) => {
return {
evaluate,
// eslint-disable-next-line
jsx: (nodeValue, explanation) => (
<MecanismEncadrement nodeValue={nodeValue} explanation={explanation} />
jsx: (nodeValue, explanation, _, unit) => (
<MecanismEncadrement
nodeValue={nodeValue}
explanation={explanation}
unit={unit}
/>
),
explanation,
category: 'mecanism',

View File

@ -1,6 +1,8 @@
import { typeWarning } from 'Engine/error'
import { evaluateNode, makeJsx, mergeMissing } from 'Engine/evaluation'
import { Node } from 'Engine/mecanismViews/common'
import { inferUnit } from 'Engine/units'
import { convertNodeToUnit } from 'Engine/nodeUnits'
import { inferUnit, serialiseUnit } from 'Engine/units'
import { curry, map } from 'ramda'
import React from 'react'
import { convertToDateIfNeeded } from '../date.ts'
@ -11,31 +13,63 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => {
curry(evaluateNode)(cache, situation, parsedRules),
node.explanation
)
let [node1, node2] = explanation
const missingVariables = mergeMissing(
explanation[0].missingVariables,
explanation[1].missingVariables
node1.missingVariables,
node2.missingVariables
)
const value1 = explanation[0].nodeValue
const value2 = explanation[1].nodeValue
if (value1 == null || value2 == null) {
if (node1.nodeValue == null || node2.nodeValue == null) {
return { ...node, nodeValue: null, explanation, missingVariables }
}
let nodeValue = operatorFunction(...convertToDateIfNeeded(value1, value2))
if (!['', '×'].includes(node.operator)) {
try {
if (node1.unit) {
node2 = convertNodeToUnit(node1.unit, node2)
} else if (node2.unit) {
node1 = convertNodeToUnit(node2.unit, node1)
}
} catch (e) {
typeWarning(
cache._meta.contextRule,
`Dans l'expression '${
node.operator
}', la partie gauche (unité: ${serialiseUnit(
node1.unit
)}) n'est pas compatible avec la partie droite (unité: ${serialiseUnit(
node2.unit
)})`,
e
)
}
}
let nodeValue = operatorFunction(
...convertToDateIfNeeded(node1.nodeValue, node2.nodeValue)
)
let unit = inferUnit(k, [node1.unit, node2.unit])
// if (node1.name === 'revenu professionnel') {
// console.log(
// node1.name,
// node2.name,
// serialiseUnit(node1.unit),
// serialiseUnit(node2.unit),
// serialiseUnit(unit)
// )
// }
return {
...node,
nodeValue,
unit,
explanation,
missingVariables
}
}
let explanation = v.explanation.map(recurse)
let [node1, node2] = explanation
let unit = inferUnit(k, [node1.unit, node2.unit])
let unit = inferUnit(k, [explanation[0].unit, explanation[1].unit])
let jsx = (nodeValue, explanation) => (
let jsx = (nodeValue, explanation, _, unit) => (
<Node
classes={'inlineExpression ' + k}
value={nodeValue}

View File

@ -80,6 +80,7 @@ export default (recurse, k, v, devariate) => {
return {
...node,
nodeValue,
...(satisfiedVariation && { unit: satisfiedVariation?.consequence.unit }),
explanation: resolvedExplanation,
missingVariables
}
@ -93,7 +94,10 @@ export default (recurse, k, v, devariate) => {
category: 'mecanism',
name: 'variations',
type: 'numeric',
unit: inferUnit('+', explanation.map(r => r.consequence.unit))
unit: inferUnit(
'+',
explanation.map(r => r.consequence.unit)
)
}
}

View File

@ -0,0 +1,46 @@
import {
areUnitConvertible,
convertUnit,
simplifyUnitWithValue,
Unit
} from './units'
export function simplifyNodeUnit(node) {
if (!node.unit || !node.nodeValue) {
return node
}
const [unit, nodeValue] = simplifyUnitWithValue(node.unit, node.nodeValue)
return {
...node,
unit,
nodeValue
}
}
export const getNodeDefaultUnit = (node, cache) => {
if (
node.question &&
node.unit == null &&
node.defaultUnit == null &&
!node.formule?.unit == null
) {
return false
}
return (
node.unit ||
cache._meta.defaultUnits.find(unit =>
areUnitConvertible(node.defaultUnit, unit)
) ||
node.defaultUnit
)
}
export function convertNodeToUnit(to: Unit, node) {
return {
...node,
nodeValue: node.unit
? convertUnit(node.unit, to, node.nodeValue)
: node.nodeValue,
unit: to
}
}

View File

@ -29,6 +29,7 @@ import {
without
} from 'ramda'
import React from 'react'
import { syntaxError } from './error.ts'
import grammar from './grammar.ne'
import {
mecanismAllOf,
@ -37,7 +38,6 @@ import {
mecanismInversion,
mecanismMax,
mecanismMin,
mecanismNumericalSwitch,
mecanismOneOf,
mecanismOnePossibility,
mecanismProduct,
@ -73,18 +73,11 @@ export let parseString = (rules, rule, parsedRules) => rawNode => {
let [parseResult] = new Parser(compiledGrammar).feed(rawNode).results
return parseObject(rules, rule, parsedRules)(parseResult)
} catch (e) {
throw new Error(`
Erreur syntaxique
=================
Dans la règle \`${rule.dottedName}\`,
\`${rawNode}\` n'est pas une formule valide
-----------------
${e.message}
`)
syntaxError(
rule.dottedName,
`\`${rawNode}\` n'est pas une formule valide`,
e
)
}
}
@ -144,13 +137,12 @@ export let parseObject = (rules, rule, parsedRules) => rawNode => {
let dispatch = {
'une de ces conditions': mecanismOneOf,
'toutes ces conditions': mecanismAllOf,
'aiguillage numérique': mecanismNumericalSwitch,
somme: mecanismSum,
multiplication: mecanismProduct,
barème,
'barème linéaire': barèmeLinéaire,
'barème continu': barèmeContinu,
encadrement: encadrement,
encadrement,
'le maximum de': mecanismMax,
'le minimum de': mecanismMin,
complément: mecanismComplement,
@ -171,14 +163,14 @@ export let parseObject = (rules, rule, parsedRules) => rawNode => {
}),
variable: () =>
parseReferenceTransforms(rules, rule, parsedRules)({ variable: v }),
temporalTransform: () =>
unitConversion: () =>
parseReferenceTransforms(
rules,
rule,
parsedRules
)({
variable: v.explanation,
temporalTransform: v.temporalTransform
unit: v.unit
}),
constant: () => ({
type: v.type,
@ -190,6 +182,8 @@ export let parseObject = (rules, rule, parsedRules) => rawNode => {
{formatValue({
unit: v.unit,
value: v.nodeValue,
// TODO : handle localization here
language: 'fr',
// We want to display constants with full precision,
// espacilly for percentages like APEC 0,036 %
maximumFractionDigits: 5

View File

@ -1,11 +1,14 @@
// Reference to a variable
import parseRule from 'Engine/parseRule'
import React from 'react'
import { typeWarning } from './error'
import { evaluateApplicability } from './evaluateRule'
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 { areUnitConvertible } from './units'
const getApplicableReplacements = (
filter,
contextRuleName,
@ -75,6 +78,19 @@ const getApplicableReplacements = (
? evaluateNode(cache, situation, rules, replacementNode)
: evaluateReference(filter)(cache, situation, rules, referenceNode)
)
.map(replacementNode => {
const replacedRuleUnit = getNodeDefaultUnit(rule, cache)
if (!areUnitConvertible(replacementNode.unit, replacedRuleUnit)) {
typeWarning(
contextRuleName,
`L'unité de la règle de remplacement n'est pas compatible avec celle de la règle remplacée ${rule.dottedName}`
)
}
return {
...replacementNode,
unit: replacementNode.unit || replacedRuleUnit
}
})
return [applicableReplacements, missingVariableList]
}
@ -85,7 +101,6 @@ let evaluateReference = (filter, contextRuleName) => (
node
) => {
let rule = rules[node.dottedName]
// When a rule exists in different version (created using the `replace` mecanism), we add
// a redirection in the evaluation of references to use a potential active replacement
const [
@ -119,7 +134,10 @@ let evaluateReference = (filter, contextRuleName) => (
cache[cacheName] = {
...node,
nodeValue,
...(explanation && { explanation }),
...(explanation && {
explanation
}),
...(explanation?.unit && { unit: explanation.unit }),
missingVariables
}
return cache[cacheName]
@ -129,13 +147,15 @@ let evaluateReference = (filter, contextRuleName) => (
missingVariables: condMissingVariables
} = evaluateApplicability(cache, situation, rules, rule)
if (!isApplicable) {
return cacheNode(isApplicable, condMissingVariables)
return cacheNode(isApplicable, condMissingVariables, rule)
}
const situationValue = getSituationValue(situation, dottedName, rule)
if (situationValue !== undefined) {
const unit = getNodeDefaultUnit(rule, cache)
return cacheNode(situationValue, condMissingVariables, {
...rule,
nodeValue: situationValue
nodeValue: situationValue,
unit
})
}
@ -166,11 +186,12 @@ export let parseReference = (
// 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))
const unit =
parsedRule.unit || parsedRule.formule?.unit || parsedRule.defaultUnit
return {
evaluate: evaluateReference(filter, rule.dottedName),
//eslint-disable-next-line react/display-name
jsx: nodeValue => (
jsx: (nodeValue, explanation, _, nodeUnit) => (
<>
<Leaf
classes="variable filtered"
@ -178,22 +199,21 @@ export let parseReference = (
name={partialReference}
dottedName={dottedName}
nodeValue={nodeValue}
unit={parsedRule.unit}
unit={nodeUnit || explanation?.unit || unit}
/>
</>
),
name: partialReference,
category: 'reference',
partialReference,
dottedName,
unit: parsedRule.unit
unit
}
}
// This function is a wrapper that can apply :
// - temporal transformations to the value of the variable.
// See the période.yaml test suite for details
// - unit transformations to the value of the variable.
// See the unité-temporelle.yaml test suite for details
// - filters on the variable to select one part of the variable's 'composantes'
const evaluateTransforms = (originalEval, rule, parseResult) => (
@ -211,61 +231,24 @@ const evaluateTransforms = (originalEval, rule, parseResult) => (
parsedRules,
node
)
if (!filteredNode.explanation) {
const { explanation, nodeValue } = filteredNode
if (!explanation || nodeValue === null) {
return filteredNode
}
let nodeValue = filteredNode.nodeValue
// Temporal transformation
let supportedPeriods = ['mois', 'année', 'flexible']
if (nodeValue == null) return filteredNode
let ruleToTransform = parsedRules[filteredNode.explanation.dottedName]
let inlinePeriodTransform = { mensuel: 'mois', annuel: 'année' }[
parseResult.temporalTransform
]
// Exceptions
if (!rule.période && !inlinePeriodTransform && rule.formule) {
if (supportedPeriods.includes(ruleToTransform?.période))
throw new Error(
`Attention, une variable sans période, ${rule.dottedName}, qui appelle une variable à période, ${ruleToTransform.dottedName}, c'est suspect !
Si la période de la variable appelée est neutralisée dans la formule de calcul, par exemple un montant mensuel divisé par 30 (comprendre 30 jours), utilisez "période: aucune" pour taire cette erreur et rassurer tout le monde.
`
const unit = parseResult.unit
if (unit) {
try {
return convertNodeToUnit(unit, filteredNode)
} catch (e) {
typeWarning(
cache._meta.contextRule,
`Impossible de convertir la reference '${filteredNode.name}'`,
e
)
return filteredNode
}
if (!ruleToTransform?.période) return filteredNode
let environmentPeriod = situation('période') || 'mois'
let callingPeriod =
inlinePeriodTransform ||
(rule.période === 'flexible' ? environmentPeriod : rule.période)
let calledPeriod =
ruleToTransform.période === 'flexible'
? environmentPeriod
: ruleToTransform.période
let transformedNodeValue =
callingPeriod === 'mois' && calledPeriod === 'année'
? nodeValue / 12
: callingPeriod === 'année' && calledPeriod === 'mois'
? nodeValue * 12
: nodeValue,
periodTransform = nodeValue !== transformedNodeValue
let result = {
...filteredNode,
periodTransform,
...(periodTransform ? { originPeriodValue: nodeValue } : {}),
nodeValue: transformedNodeValue,
explanation: filteredNode.explanation,
missingVariables: filteredNode.missingVariables
}
}
return result
return filteredNode
}
export let parseReferenceTransforms = (
rules,
@ -273,9 +256,12 @@ export let parseReferenceTransforms = (
parsedRules
) => parseResult => {
const referenceName = parseResult.variable.fragments.join(' . ')
let node = parseReference(rules, rule, parsedRules, parseResult.filter)(
referenceName
)
let node = parseReference(
rules,
rule,
parsedRules,
parseResult.filter
)(referenceName)
return {
...node,
@ -289,6 +275,7 @@ export let parseReferenceTransforms = (
}
}
: {}),
evaluate: evaluateTransforms(node.evaluate, rule, parseResult)
evaluate: evaluateTransforms(node.evaluate, rule, parseResult),
unit: parseResult.unit || node.unit
}
}

View File

@ -78,10 +78,8 @@ export default (rules, rule, parsedRules) => {
parsedRules,
node.explanation
),
nodeValue = explanation.nodeValue,
missingVariables = explanation.missingVariables
return { ...node, nodeValue, explanation, missingVariables }
{ nodeValue, unit, missingVariables } = explanation
return { ...node, nodeValue, unit, missingVariables, explanation }
}
let child = parse(rules, rule, parsedRules)(value)
@ -94,7 +92,7 @@ export default (rules, rule, parsedRules) => {
category: 'ruleProp',
rulePropType: 'formula',
name: 'formule',
type: 'numeric',
unit: child.unit,
explanation: child
}
},
@ -125,10 +123,9 @@ export default (rules, rule, parsedRules) => {
evaluate,
parsed: true,
isDisabledBy: [],
replacedBy: [],
unit: rule.unit || parsedRoot.formule?.explanation?.unit
defaultUnit: parsedRoot.defaultUnit || parsedRoot.formule?.unit,
replacedBy: []
}
parsedRules[rule.dottedName]['rendu non applicable'] = {
evaluate: (cache, situation, parsedRules, node) => {
const isDisabledBy = node.explanation.isDisabledBy.map(disablerNode =>
@ -162,7 +159,6 @@ export default (rules, rule, parsedRules) => {
type: 'boolean',
explanation: parsedRules[rule.dottedName]
}
return parsedRules[rule.dottedName]
}

View File

@ -29,6 +29,7 @@ import rawRules from 'Règles/base.yaml'
import translations from 'Règles/externalized.yaml'
// TODO - should be in UI, not engine
import { capitalise0, coerceArray } from '../utils'
import { syntaxError, warning } from './error'
import possibleVariableTypes from './possibleVariableTypes.yaml'
/***********************************
@ -36,9 +37,20 @@ Functions working on one rule */
export let enrichRule = rule => {
try {
let unit = rule.unité && parseUnit(rule.unité)
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é` n'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,
@ -49,11 +61,15 @@ export let enrichRule = rule => {
examples: rule['exemples'],
icons: rule['icônes'],
summary: rule['résumé'],
unit
unit,
defaultUnit
}
} catch (e) {
console.log(e)
throw new Error('Problem enriching ' + JSON.stringify(rule))
syntaxError(
rule.dottedName || rule.nom,
'Problème dans la lecture des champs de la règle',
e
)
}
}
@ -71,15 +87,8 @@ 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 parentName = pipe(splitName, dropLast(1), joinName)
export let nameLeaf = pipe(splitName, last)
export let encodeRuleName = name =>
encodeURI(
@ -117,9 +126,10 @@ export let disambiguateRuleReference = (
found = reduce(
(res, path) => {
let dottedNameToCheck = [...path, partialName].join(' . ')
return when(is(Object), reduced)(
findRuleByDottedName(allRules, dottedNameToCheck)
)
return when(
is(Object),
reduced
)(findRuleByDottedName(allRules, dottedNameToCheck))
},
null,
pathPossibilities

View File

@ -8,6 +8,7 @@ import {
findRule,
findRuleByDottedName
} from './rules'
import { parseUnit } from './units'
/*
Dans ce fichier, les règles YAML sont parsées.
@ -117,10 +118,17 @@ export let getTargets = (target, rules) => {
return targets
}
export let analyseMany = (parsedRules, targetNames) => situationGate => {
export let analyseMany = (
parsedRules,
targetNames,
defaultUnits = []
) => situationGate => {
// TODO: we should really make use of namespaces at this level, in particular
// setRule in Rule.js needs to get smarter and pass dottedName
let cache = { parseLevel: 0 }
defaultUnits = defaultUnits.map(parseUnit)
let cache = {
_meta: { parseLevel: 0, contextRule: [], defaultUnits }
}
let parsedTargets = targetNames.map(t => {
let parsedTarget = findRule(parsedRules, t)
@ -137,10 +145,9 @@ export let analyseMany = (parsedRules, targetNames) => situationGate => {
)
let controls = evaluateControls(cache, situationGate, parsedRules)
return { targets, cache, controls }
}
export let analyse = (parsedRules, target) => {
return analyseMany(parsedRules, [target])
export let analyse = (parsedRules, target, defaultUnits = []) => {
return analyseMany(parsedRules, [target], defaultUnits)
}

View File

@ -1,4 +1,16 @@
import { isEmpty, remove, unnest } from 'ramda'
import {
countBy,
equals,
flatten,
isEmpty,
keys,
map,
pipe,
remove,
uniq,
unnest,
without
} from 'ramda'
import i18n from '../i18n'
type BaseUnit = string
@ -10,10 +22,13 @@ export type Unit = {
//TODO this function does not handle complex units like passenger-kilometer/flight
export let parseUnit = (string: string): Unit => {
let [a, b = ''] = string.split('/'),
let [a, ...b] = string.split('/'),
result = {
numerators: a !== '' ? [getUnitKey(a)] : [],
denominators: b !== '' ? [getUnitKey(b)] : []
numerators: a
.split('.')
.filter(Boolean)
.map(getUnitKey),
denominators: b.map(getUnitKey)
}
return result
}
@ -32,7 +47,7 @@ let printUnits = (units: Array<string>, count: number): string =>
units
.filter(unit => unit !== '%')
.map(unit => i18n.t(`units:${unit}`, { count }))
.join('-')
.join('.')
const plural = 2
export let serialiseUnit = (
@ -97,20 +112,166 @@ export let inferUnit = (
return null
}
export let removeOnce = <T>(element: T) => (list: Array<T>): Array<T> => {
let index = list.indexOf(element)
export let removeOnce = <T>(
element: T,
eqFn: (a: T, b: T) => boolean = equals
) => (list: Array<T>): Array<T> => {
let index = list.findIndex(e => eqFn(e, element))
if (index > -1) return remove<T>(index, 1)(list)
else return list
}
let simplify = (unit: Unit): Unit =>
let simplify = (
unit: Unit,
eqFn: (a: string, b: string) => boolean = equals
): Unit =>
[...unit.numerators, ...unit.denominators].reduce(
({ numerators, denominators }, next) =>
numerators.includes(next) && denominators.includes(next)
numerators.find(u => eqFn(next, u)) &&
denominators.find(u => eqFn(next, u))
? {
numerators: removeOnce(next)(numerators),
denominators: removeOnce(next)(denominators)
numerators: removeOnce(next, eqFn)(numerators),
denominators: removeOnce(next, eqFn)(denominators)
}
: { numerators, denominators },
unit
)
const convertTable: { readonly [index: string]: number } = {
'mois/an': 12,
'€/k€': 1000,
'jour/an': 365,
'jour/mois': 365 / 12,
'trimestre/an': 4,
'mois/trimestre': 3,
'jour/trimestre': (365 / 12) * 3
}
function singleUnitConversionFactor(
from: string,
to: string
): number | undefined {
return (
convertTable[`${to}/${from}`] ||
(convertTable[`${from}/${to}`] && 1 / convertTable[`${from}/${to}`])
)
}
function unitsConversionFactor(from: string[], to: string[]): number {
let factor = 1
if (to.includes('%')) {
factor *= 100
}
if (from.includes('%')) {
factor /= 100
}
;[factor] = from.reduce(
([value, toUnits], fromUnit) => {
const index = toUnits.findIndex(
toUnit => !!singleUnitConversionFactor(fromUnit, toUnit)
)
const factor = singleUnitConversionFactor(fromUnit, toUnits[index]) || 1
return [
value * factor,
[...toUnits.slice(0, index + 1), ...toUnits.slice(index + 1)]
]
},
[factor, to]
)
return factor
}
export function convertUnit(from: Unit, to: Unit, value: number) {
if (!areUnitConvertible(from, to)) {
throw new Error(
`Impossible de convertir l'unité '${serialiseUnit(
from
)}' en '${serialiseUnit(to)}'`
)
}
if (!value) {
return value
}
const [fromSimplified, factorTo] = simplifyUnitWithValue(from)
const [toSimplified, factorFrom] = simplifyUnitWithValue(to)
return round(
((value * factorTo) / factorFrom) *
unitsConversionFactor(
fromSimplified.numerators,
toSimplified.numerators
) *
unitsConversionFactor(
toSimplified.denominators,
fromSimplified.denominators
)
)
}
const convertibleUnitClasses = [
['mois', 'an', 'jour', 'trimestre'],
['€', 'k€']
]
function areSameClass(a: string, b: string) {
return (
a === b ||
convertibleUnitClasses.some(units => units.includes(a) && units.includes(b))
)
}
function round(value: number) {
return +value.toFixed(16)
}
export function simplifyUnitWithValue(
unit: Unit,
value: number = 1
): [Unit, number] {
const { denominators, numerators } = unit
const factor = unitsConversionFactor(numerators, denominators)
return [
simplify(
{
numerators: without(['%'], numerators),
denominators: without(['%'], denominators)
},
areSameClass
),
value ? round(value * factor) : value
]
}
export function areUnitConvertible(a: Unit, b: Unit) {
if (a == null || b == null) {
return true
}
const countByUnitClass = countBy((unit: string) => {
const classIndex = convertibleUnitClasses.findIndex(unitClass =>
unitClass.includes(unit)
)
return classIndex === -1 ? unit : '' + classIndex
})
const [numA, denomA, numB, denomB] = [
a.numerators,
a.denominators,
b.numerators,
b.denominators
].map(countByUnitClass)
const unitClasses = pipe(
map(keys),
flatten,
uniq
)([numA, denomA, numB, denomB])
return unitClasses.every(
unitClass =>
(numA[unitClass] || 0) - (denomA[unitClass] || 0) ===
(numB[unitClass] || 0) - (denomB[unitClass] || 0) || unitClass === '%'
)
}
export function isPercentUnit(unit: Unit) {
if (!unit) {
return false
}
const simplifiedUnit = simplifyUnitWithValue(unit)[0]
return (
simplifiedUnit.denominators.length === 0 &&
simplifiedUnit.numerators.length === 0
)
}

View File

@ -58,7 +58,6 @@ Cotisations sociales: Social contributions
Part employeur: Employer share
Part salariale: Employee share
Total des retenues: Total withheld
Fiche de paie mensuelle: Monthly payslip
Fiche de paie: Payslip
Détail annuel des cotisations: Annual detail of my contributions
Voir la répartition des cotisations: View contribution breakdown

View File

@ -1,5 +1,5 @@
import { Action } from 'Actions/actions'
import { findRuleByDottedName } from 'Engine/rules'
import { areUnitConvertible, convertUnit, parseUnit } from 'Engine/units'
import {
compose,
defaultTo,
@ -14,10 +14,11 @@ import {
} from 'ramda'
import reduceReducers from 'reduce-reducers'
import { combineReducers, Reducer } from 'redux'
import { targetNamesSelector } from 'Selectors/analyseSelectors'
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
import { SavedSimulation } from 'Selectors/storageSelectors'
import { DottedName, Rule } from 'Types/rule'
import i18n, { AvailableLangs } from '../i18n'
import { Unit } from './../engine/units'
import inFranceAppReducer from './inFranceAppReducer'
import storageRootReducer from './storageReducer'
@ -92,7 +93,7 @@ function conversationSteps(
},
action: Action
): ConversationSteps {
if (action.type === 'RESET_SIMULATION')
if (['RESET_SIMULATION', 'SET_SIMULATION'].includes(action.type))
return { foldedSteps: [], unfoldedStep: null }
if (action.type !== 'STEP_ACTION') return state
@ -111,48 +112,45 @@ function conversationSteps(
return state
}
function updateSituation(situation, { fieldName, value, config, rules }) {
const goals = targetNamesSelector({ simulation: { config } } as any).filter(
dottedName => {
const target = rules.find(r => r.dottedName === dottedName)
const isSmallTarget = !target.question || !target.formule
return !isSmallTarget
}
)
function updateSituation(situation, { fieldName, value, analysis }) {
const goals = analysis.targets
.map(target => target.explanation || target)
.filter(target => !!target.formule == !!target.question)
.map(({ dottedName }) => dottedName)
const removePreviousTarget = goals.includes(fieldName)
? omit(goals)
: identity
return { ...removePreviousTarget(situation), [fieldName]: value }
}
function updatePeriod(situation, { toPeriod, rules }) {
const currentPeriod = situation['période']
if (currentPeriod === toPeriod) {
return situation
}
if (!['mois', 'année'].includes(toPeriod)) {
throw new Error('Oups, changement de période invalide')
}
function updateDefaultUnit(situation, { toUnit, analysis }) {
const unit = parseUnit(toUnit)
const needConversion = Object.keys(situation).filter(dottedName => {
const rule = findRuleByDottedName(rules, dottedName)
return rule?.période === 'flexible'
})
const updatedSituation = Object.entries(situation)
.filter(([fieldName]) => needConversion.includes(fieldName))
.map(([fieldName, value]) => [
fieldName,
currentPeriod === 'mois' && toPeriod === 'année'
? (value as number) * 12
: (value as number) / 12
])
return {
...situation,
...Object.fromEntries(updatedSituation),
période: toPeriod
}
const convertedSituation = Object.keys(situation)
.map(
dottedName =>
analysis.targets.find(target => target.dottedName === dottedName) ||
analysis.cache[dottedName]
)
.filter(
rule =>
(rule.unit || rule.defaultUnit) &&
!rule.unité &&
!rule.explanation?.unité &&
areUnitConvertible(rule.unit || rule.defaultUnit, unit)
)
.reduce(
(convertedSituation, rule) => ({
...convertedSituation,
[rule.dottedName]: convertUnit(
rule.unit || rule.defaultUnit,
unit,
situation[rule.dottedName]
)
}),
situation
)
return convertedSituation
}
type QuestionsKind =
@ -169,6 +167,7 @@ export type SimulationConfig = Partial<{
bloquant: Array<DottedName>
situation: Simulation['situation']
branches: Array<{ nom: string; situation: SimulationConfig['situation'] }>
defaultUnits: [string]
}>
export type Simulation = {
@ -176,16 +175,27 @@ export type Simulation = {
url: string
hiddenControls: Array<string>
situation: Record<DottedName, any>
defaultUnits: [string]
}
function simulation(
state: Simulation = null,
action: Action,
rules: Array<Rule>
analysis: Record<DottedName, { nodeValue: any; unit: Unit | undefined }>
): Simulation | null {
if (action.type === 'SET_SIMULATION') {
const { config, url } = action
return { config, url, hiddenControls: [], situation: {} }
if (state && state.config === config) {
return state
}
return {
config,
url,
hiddenControls: [],
situation: {},
defaultUnits: (state && state.defaultUnits) ||
config.defaultUnits || ['€/mois']
}
}
if (state === null) {
return state
@ -201,42 +211,34 @@ function simulation(
situation: updateSituation(state.situation, {
fieldName: action.fieldName,
value: action.value,
config: state.config,
rules
analysis
})
}
case 'UPDATE_PERIOD':
case 'UPDATE_DEFAULT_UNIT':
return {
...state,
situation: updatePeriod(state.situation, {
toPeriod: action.toPeriod,
rules
defaultUnits: [action.defaultUnit],
situation: updateDefaultUnit(state.situation, {
toUnit: action.defaultUnit,
analysis
})
}
}
return state
}
const addAnswerToSituation = (
dottedName: DottedName,
value: any,
state: RootState
) => {
console.log(state)
const addAnswerToSituation = (dottedName: DottedName, value: any, state) => {
return (compose(
set(lensPath(['simulation', 'config', 'situation', dottedName]), value),
set(lensPath(['simulation', 'situation', dottedName]), value),
over(lensPath(['conversationSteps', 'foldedSteps']), (steps = []) =>
uniq([...steps, dottedName])
) as any
) as any)(state)
}
const removeAnswerFromSituation = (
dottedName: DottedName,
state: RootState
) => {
const removeAnswerFromSituation = (dottedName: DottedName, state) => {
return (compose(
over(lensPath(['simulation', 'config', 'situation']), dissoc(dottedName)),
over(lensPath(['simulation', 'situation']), dissoc(dottedName)),
over(
lensPath(['conversationSteps', 'foldedSteps']),
without([dottedName])
@ -244,7 +246,7 @@ const removeAnswerFromSituation = (
) as any)(state)
}
const existingCompanyRootReducer = (state: RootState, action): RootState => {
const existingCompanyRootReducer = (state: RootState, action) => {
if (!action.type.startsWith('EXISTING_COMPANY::')) {
return state
}
@ -268,8 +270,8 @@ const mainReducer = (state, action: Action) =>
rules: defaultTo(null) as Reducer<Array<Rule>>,
explainedVariable,
// We need to access the `rules` in the simulation reducer
simulation: (a: Simulation | null, b: Action) =>
simulation(a, b, state.rules),
simulation: (a: Simulation | null, b: Action): Simulation =>
simulation(a, b, a && analysisWithDefaultsSelector(state)),
previousSimulation: defaultTo(null) as Reducer<SavedSimulation>,
currentExample,
situationBranch,

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
```yaml
aiguillage numérique: # première valeur trouvée, sinon 0
- poursuite du CDD en CDI: 0%
# - Contrat . type : # mécanisme de match à introduire une fois les entités gérées. Exclusivité exprimée dans l'entité Type
- conditions exclusives:
# ce n'est pas évident de savoir le type d'un CDD, proposer le calcul dans une autre variable !!
- CDD type accroissement temporaire d'activité:
- contrat de travail durée ≤ 1 mois: 3%
- contrat de travail durée ≤ 3 mois: 1.5%
- CDD type usage:
- contrat de travail durée ≤ 3 mois: 0.5%
# - True: 0% # Ce mécanisme ajoute automatiquement cette ligne :)
aiguillage numérique 2:
- poursuite du CDD en CDI: 0%
- aiguillage: # signale que les deux propositions sont exclusives
sujet: Contrat . type
propositions:
- accroissement temporaire d'activité:
- contrat de travail durée ≤ 1 mois: 3%
- contrat de travail durée ≤ 3 mois: 1.5%
- usage:
- contrat de travail durée ≤ 3 mois: 0.5%
```
On aurait aussi pu écrire la formule de façon plus explicite mais plus verbose :
```yaml
variations:
- si: motif . accroissement temporaire activité:
variations:
- si: durée contrat <= 1
taux: 3%
- si: durée contrat <= 3
taux: 1.5%
- si: motif . usage:
variations:
- si: durée contrat <= 3
taux: 0.5%
```

View File

@ -1,37 +1,30 @@
# espace de nom implicite : douche
# non bloquant :
# - période: semaine
# bloquant :
# - ?
douche:
icônes: 🚿
douche . impact:
icônes: 🍃
période: flexible
unité: kgCO2eq
formule: impact par douche * douche . nombre
douche . nombre:
période: flexible
question: Combien prenez-vous de douches ?
unité: _
unité: douche
par défaut: 30
suggestions:
Une par jour: 30
douche . impact par douche:
formule: impact par litre * litres d'eau
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:
douche . litres d'eau par douche:
icônes: 🇱
formule: durée de la douche * litres par minute
formule: durée de la douche * litres par minute / 1 douche
douche . litres par minute:
unité: l/min
formule:
variations:
- si: pomme de douche économe
@ -113,7 +106,7 @@ chauffage . impact par litre:
douche . durée de la douche:
question: Combien de temps dure votre douche en général ?
unité: _
unité: min
par défaut: 5
suggestions:
expresse: 5

View File

@ -698,9 +698,7 @@ contrat salarié . rémunération . brut de base:
contrôles.en:
- si:
toutes ces conditions:
- >-
rémunération . assiette de vérification du SMIC [mensuel] < SMIC
[mensuel]
- rémunération . assiette de vérification du SMIC < SMIC
- dirigeant != 'assimilé salarié'
- stage != oui
- apprentissage != oui
@ -710,40 +708,18 @@ contrat salarié . rémunération . brut de base:
solution:
cible: contrat salarié . temps partiel
texte: Is it a part-time contract?
- si:
toutes ces conditions:
- 'brut de base [mensuel] > 10000'
- période = 'mois'
- si: brut de base > 10000 €/mois
niveau: information
message: >
The monthly wage seized is high. Are you sure the calculation period
isn't set to month instead of year?
contrôles.fr:
- si:
toutes ces conditions:
- >-
rémunération . assiette de vérification du SMIC [mensuel] < SMIC
contractuel [mensuel]
- dirigeant != 'assimilé salarié'
- stage != oui
- apprentissage != oui
niveau: avertissement
message: |
- message: |
Le salaire saisi est inférieur au SMIC.
- si:
toutes ces conditions:
- stage
- 'brut de base [mensuel] < stage . gratification minimale [mensuel]'
niveau: avertissement
message: >
- message: >
La rémunération du stage est inférieure à la [gratification
minimale](https://www.service-public.fr/professionnels-entreprises/vosdroits/F32131).
- si:
toutes ces conditions:
- 'brut de base [mensuel] > 10000'
- période = 'mois'
niveau: information
message: >
- message: >
Le salaire mensuel saisi est élevé. Ne vous êtes-vous pas trompé de
période de calcul ?
contrat salarié . rémunération . brut de base . équivalent temps plein:
@ -1541,11 +1517,11 @@ contrat salarié . temps de travail . heures supplémentaires:
contrôles.en:
- si:
toutes ces conditions:
- heures supplémentaires > 9 * 4.33
- heures supplémentaires <= 13 * 4.33
- heures supplémentaires > 9 heures/semaine * période . semaines par mois
- heures supplémentaires <= 13 heures/semaine * période . semaines par mois
niveau: info
message: The average weekly working time may not exceed 44 hours
- si: heures supplémentaires > 13 * 4.33
- si: heures supplémentaires > 13 heures/semaine * période . semaines par mois
niveau: avertissement
message: The maximum weekly working time may not exceed 48 hours
contrôles.fr:
@ -1803,17 +1779,15 @@ contrat salarié . complémentaire santé . forfait:
ce que nous avons retenu pour cette simulation, ou davantage. Le montant est
libre, tant qu'elle couvre un panier légal de soins.
contrôles.en:
- si: 'complémentaire santé . forfait [mensuel] < 15'
- si: 'complémentaire santé . forfait < 15€/mois'
message: >-
Make sure that such an inexpensive health supplement covers the minimum
care basket defined in the law.
niveau: avertissement
contrôles.fr:
- si: 'complémentaire santé . forfait [mensuel] < 15'
message: >-
- message: >-
Vérifiez bien qu'une complémentaire santé si peu chère couvre le panier
de soin minimal défini dans la loi.
niveau: avertissement
contrat salarié . complémentaire santé . forfait . en alsace moselle:
titre.en: Complementary health insurance plan (Alsace-Moselle)
titre.fr: forfait complémentaire santé en Alsace-Moselle
@ -3155,7 +3129,7 @@ dirigeant . auto-entrepreneur . impôt . versement libératoire:
contrôles.en:
- si:
toutes ces conditions:
- 'impôt . revenu fiscal de référence [annuel] > 27086'
- 'impôt . revenu fiscal de référence > 27086 €/an'
- versement libératoire
message: >-
The discharge payment is not available if your household income exceeds
@ -3164,7 +3138,7 @@ dirigeant . auto-entrepreneur . impôt . versement libératoire:
contrôles.fr:
- si:
toutes ces conditions:
- 'impôt . revenu fiscal de référence [annuel] > 27086'
- 'impôt . revenu fiscal de référence > 27086 €/an'
- versement libératoire
message: >-
Le versement libératoire n'est pas disponible si les revenus de votre

View File

@ -1,22 +1,19 @@
# Ce petit ensemble de règles a été historiquement utilisé pour tester l'externalisation du moteur, et est en train d'être réintégré progressivement dans la base centrale
chiffre affaires:
période: flexible
unité:
unité par défaut: €/mois
charges:
période: flexible
par défaut: 0
unité:
par défaut: 0 €/mois
répartition salaire sur dividendes:
par défaut: 0.5
par défaut: 50
unité: '%'
impôt sur les sociétés:
période: année
formule:
barème:
assiette: bénéfice
assiette: bénéfice [€/an]
tranches:
- en-dessous de: 38120
taux: 15%
@ -29,21 +26,17 @@ impôt sur les sociétés:
fiche service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23575
bénéfice:
période: flexible
formule: chiffre affaires - salaire total
dividendes:
dividendes . brut:
période: flexible
formule: bénéfice - impôt sur les sociétés
dividendes . net:
période: flexible
formule: brut - prélèvement forfaitaire unique
dividendes . prélèvement forfaitaire unique:
période: flexible
formule:
multiplication:
assiette: brut
@ -52,9 +45,7 @@ dividendes . prélèvement forfaitaire unique:
- taux: 12.8%
salaire total:
période: flexible
formule: chiffre affaires * répartition salaire sur dividendes
revenu net après impôt:
période: flexible
formule: contrat salarié . rémunération . net après impôt + dividendes . net

View File

@ -1,23 +1,50 @@
import { collectMissingVariablesByTarget, getNextSteps } from 'Engine/generateQuestions'
import { collectDefaults, disambiguateExampleSituation, findRuleByDottedName } from 'Engine/rules'
import {
collectMissingVariablesByTarget,
getNextSteps
} from 'Engine/generateQuestions'
import {
collectDefaults,
disambiguateExampleSituation,
findRuleByDottedName
} from 'Engine/rules'
import { analyse, analyseMany, parseAll } from 'Engine/traverse'
import { add, defaultTo, difference, dissoc, equals, head, intersection, isEmpty, isNil, last, length, map, mergeDeepWith, negate, pick, pipe, sortBy, split, takeWhile, zipWith } from 'ramda'
import {
add,
defaultTo,
difference,
equals,
head,
intersection,
isNil,
last,
length,
map,
mergeDeepWith,
negate,
pick,
pipe,
sortBy,
split,
takeWhile,
zipWith
} from 'ramda'
import { useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'
import { DottedName } from "Types/rule"
import { DottedName } from 'Types/rule'
import { mapOrApply } from '../utils'
// create a "selector creator" that uses deep equal instead of ===
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, equals)
let configSelector = (state: RootState) => state.simulation && state.simulation.config || {}
let configSelector = (state: RootState) =>
(state.simulation && state.simulation.config) || {}
// We must here compute parsedRules, flatRules, analyse which contains both targets and cache objects
export let flatRulesSelector = (
state: RootState,
props?: { rules: RootState['rules'] }
) => {
return props && props.rules || state.rules
return (props && props.rules) || state.rules
}
export let parsedRulesSelector = createSelector([flatRulesSelector], rules =>
@ -50,9 +77,7 @@ export let targetNamesSelector = (state: RootState) => {
type SituationSelectorType = typeof situationSelector
export const situationSelector = (state: RootState) =>
state.simulation && state.simulation.situation || {}
export const usePeriod = () => useSelector(situationSelector)['période']
(state.simulation && state.simulation.situation) || {}
export const useTarget = (dottedName: DottedName) => {
const targets = useSelector(
@ -61,34 +86,25 @@ export const useTarget = (dottedName: DottedName) => {
return targets && targets.find(t => t.dottedName === dottedName)
}
export let noUserInputSelector = createSelector(
[situationSelector],
situation => !situation || isEmpty(dissoc('période', situation))
)
export let noUserInputSelector = state =>
!Object.keys(situationSelector(state)).length
export let firstStepCompletedSelector = createSelector(
[
situationSelector,
targetNamesSelector,
parsedRulesSelector,
configSelector
],
[situationSelector, targetNamesSelector, parsedRulesSelector, configSelector],
(situation, targetNames, parsedRules, config) => {
if (!situation) {
return true
}
const situations = Object.keys(situation)
const allBlockingAreAnswered =
config.bloquant && config.bloquant.every(rule => situations.includes(rule))
config.bloquant &&
config.bloquant.every(rule => situations.includes(rule))
const targetIsAnswered =
targetNames &&
targetNames.some(
targetName => {
const rule = findRuleByDottedName(parsedRules, targetName)
return rule && rule.formule &&
targetName in situation
}
)
targetNames.some(targetName => {
const rule = findRuleByDottedName(parsedRules, targetName)
return rule && rule.formule && targetName in situation
})
return allBlockingAreAnswered || targetIsAnswered
}
)
@ -97,7 +113,9 @@ let validatedStepsSelector = createSelector(
[state => state.conversationSteps.foldedSteps, targetNamesSelector],
(foldedSteps, targetNames) => [...foldedSteps, ...targetNames]
)
let branchesSelector = (state: RootState) => configSelector(state).branches
export const defaultUnitsSelector = (state: RootState) =>
state.simulation?.defaultUnits || []
let branchesSelector = (state: RootState) => configSelector(state).branches
let configSituationSelector = (state: RootState) =>
configSelector(state).situation || {}
@ -144,23 +162,29 @@ export let situationsWithDefaultsSelector = createSelector(
mapOrApply(situation => ({ ...defaults, ...situation }), situations)
)
let analyseRule = (parsedRules, ruleDottedName, situationGate) =>
analyse(parsedRules, ruleDottedName)(situationGate).targets[0]
let analyseRule = (parsedRules, ruleDottedName, situationGate, defaultUnits) =>
analyse(parsedRules, ruleDottedName, defaultUnits)(situationGate).targets[0]
export let ruleAnalysisSelector = createSelector(
[
parsedRulesSelector,
(_, props) => props.dottedName,
situationsWithDefaultsSelector,
state => state.situationBranch || 0
state => state.situationBranch || 0,
defaultUnitsSelector
],
(rules, dottedName, situations, situationBranch) => {
return analyseRule(rules, dottedName, dottedName => {
const currentSituation = Array.isArray(situations)
? situations[situationBranch]
: situations
return currentSituation[dottedName]
})
(rules, dottedName, situations, situationBranch, defaultUnits) => {
return analyseRule(
rules,
dottedName,
dottedName => {
const currentSituation = Array.isArray(situations)
? situations[situationBranch]
: situations
return currentSituation[dottedName]
},
defaultUnits
)
}
)
@ -183,22 +207,34 @@ export let exampleAnalysisSelector = createSelector(
[
parsedRulesSelector,
(_, props) => props.dottedName,
exampleSituationSelector
exampleSituationSelector,
({ currentExample }) => currentExample
],
(rules, dottedName, situation) =>
(rules, dottedName, situation, example) =>
situation &&
analyseRule(rules, dottedName, dottedName => situation[dottedName])
analyseRule(
rules,
dottedName,
dottedName => situation[dottedName],
example.defaultUnits
)
)
let makeAnalysisSelector = (situationSelector: SituationSelectorType) =>
createDeepEqualSelector(
[parsedRulesSelector, targetNamesSelector, situationSelector],
(parsedRules, targetNames, situations) =>
[
parsedRulesSelector,
targetNamesSelector,
situationSelector,
defaultUnitsSelector
],
(parsedRules, targetNames, situations, defaultUnits) =>
mapOrApply(
situation =>
analyseMany(
parsedRules,
targetNames
targetNames,
defaultUnits
)(dottedName => {
return situation[dottedName]
}),
@ -262,11 +298,13 @@ export let nextStepsSelector = createSelector(
],
(
mv,
{questions: {
'non prioritaires': notPriority = [],
uniquement: only = null,
'liste noire': blacklist = []
} = {}},
{
questions: {
'non prioritaires': notPriority = [],
uniquement: only = null,
'liste noire': blacklist = []
} = {}
},
foldedSteps = [],
situation
) => {

View File

@ -38,13 +38,16 @@ export default (tracker: Tracker) => {
])
}
if (action.type === 'UPDATE_SITUATION' || action.type === 'UPDATE_PERIOD') {
if (
action.type === 'UPDATE_SITUATION' ||
action.type === 'UPDATE_DEFAULT_UNIT'
) {
tracker.push([
'trackEvent',
'Simulator',
'update situation',
...(action.type === 'UPDATE_PERIOD'
? ['période', action.toPeriod]
...(action.type === 'UPDATE_DEFAULT_UNIT'
? ['unité', action.defaultUnit]
: [action.fieldName, action.value])
])
}

View File

@ -1,18 +1,18 @@
import { updateSituation } from 'Actions/actions'
import { setSimulationConfig, updateSituation } from 'Actions/actions'
import { DistributionBranch } from 'Components/Distribution'
import RuleLink from 'Components/RuleLink'
import SimulateurWarning from 'Components/SimulateurWarning'
import config from 'Components/simulationConfigs/artiste-auteur.yaml'
import { useSimulationConfig } from 'Components/simulationConfigs/useSimulationConfig'
import 'Components/TargetSelection.css'
import { formatValue } from 'Engine/format'
import { getRuleFromAnalysis } from 'Engine/rules'
import { serialiseUnit } from 'Engine/units'
import React, { useEffect, useState } from 'react'
import NumberFormat from 'react-number-format'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
ruleAnalysisSelector,
situationSelector
} from 'Selectors/analyseSelectors'
import styled from 'styled-components'
@ -35,7 +35,8 @@ function useInitialRender() {
}
export default function ArtisteAuteur() {
useSimulationConfig(config)
const dispatch = useDispatch()
dispatch(setSimulationConfig(config))
const initialRender = useInitialRender()
return (
@ -82,13 +83,15 @@ type SimpleFieldProps = {
function SimpleField({ dottedName, initialRender }: SimpleFieldProps) {
const rule = useRule(dottedName)
const dispatch = useDispatch()
const situation = useSelector(situationSelector)
const [value, setValue] = useState(situation[dottedName])
if (!rule) {
const analysis = useSelector((state: RootState) =>
ruleAnalysisSelector(state, { dottedName })
)
const [value, setValue] = useState(analysis.nodeValue)
if (!analysis.isApplicable) {
return null
}
const unit = serialiseUnit(rule.unit)
return (
<li>
<Animate.appear unless={initialRender}>
@ -100,7 +103,8 @@ function SimpleField({ dottedName, initialRender }: SimpleFieldProps) {
</label>
</div>
<div className="targetInputOrValue">
{unit === '€' && (
{/* Super hacky */}
{analysis.unit !== undefined ? (
<NumberFormat
autoFocus
id={'step-' + dottedName}
@ -120,9 +124,7 @@ function SimpleField({ dottedName, initialRender }: SimpleFieldProps) {
padding: 10px;
`}
/>
)}
{/* Super hacky */}
{unit !== '€' && (
) : (
<ToggleSwitch
id={`step-${dottedName}`}
defaultChecked={rule.nodeValue}

View File

@ -1,15 +1,18 @@
import { setSimulationConfig } from 'Actions/actions'
import { T } from 'Components'
import SalaryExplanation from 'Components/SalaryExplanation'
import Warning from 'Components/SimulateurWarning'
import Simulation from 'Components/Simulation'
import assimiléConfig from 'Components/simulationConfigs/assimilé.yaml'
import { useSimulationConfig } from 'Components/simulationConfigs/useSimulationConfig'
import React from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
export default function AssimiléSalarié() {
useSimulationConfig(assimiléConfig)
const dispatch = useDispatch()
dispatch(setSimulationConfig(assimiléConfig))
const { t } = useTranslation()
return (

View File

@ -1,19 +1,21 @@
import { setSimulationConfig } from 'Actions/actions'
import { T } from 'Components'
import Warning from 'Components/SimulateurWarning'
import Simulation from 'Components/Simulation'
import indépendantConfig from 'Components/simulationConfigs/auto-entrepreneur.yaml'
import { useSimulationConfig } from 'Components/simulationConfigs/useSimulationConfig'
import autoEntrepreneurConfig from 'Components/simulationConfigs/auto-entrepreneur.yaml'
import StackedBarChart from 'Components/StackedBarChart'
import { ThemeColoursContext } from 'Components/utils/withColours'
import { getRuleFromAnalysis } from 'Engine/rules'
import React, { useContext } from 'react'
import { default as React, useContext } from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
export default function AutoEntrepreneur() {
useSimulationConfig(indépendantConfig)
const dispatch = useDispatch()
dispatch(setSimulationConfig(autoEntrepreneurConfig))
const { t } = useTranslation()
return (

View File

@ -1,19 +1,20 @@
import { setSimulationConfig } from 'Actions/actions'
import { T } from 'Components'
import Warning from 'Components/SimulateurWarning'
import Simulation from 'Components/Simulation'
import indépendantConfig from 'Components/simulationConfigs/indépendant.yaml'
import { useSimulationConfig } from 'Components/simulationConfigs/useSimulationConfig'
import StackedBarChart from 'Components/StackedBarChart'
import { ThemeColoursContext } from 'Components/utils/withColours'
import { getRuleFromAnalysis } from 'Engine/rules'
import React, { useContext } from 'react'
import { default as React, useContext } from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
export default function Indépendant() {
useSimulationConfig(indépendantConfig)
const dispatch = useDispatch()
dispatch(setSimulationConfig(indépendantConfig))
const { t } = useTranslation()
return (
<>

View File

@ -1,22 +1,24 @@
import { setSimulationConfig } from 'Actions/actions'
import { T } from 'Components'
import Banner from 'Components/Banner'
import PreviousSimulationBanner from 'Components/PreviousSimulationBanner'
import SalaryExplanation from 'Components/SalaryExplanation'
import Simulation from 'Components/Simulation'
import salariéConfig from 'Components/simulationConfigs/salarié.yaml'
import { useSimulationConfig } from 'Components/simulationConfigs/useSimulationConfig'
import { IsEmbeddedContext } from 'Components/utils/embeddedContext'
import { Markdown } from 'Components/utils/markdown'
import { SitePathsContext } from 'Components/utils/withSitePaths'
import urlIllustrationNetBrutEn from 'Images/illustration-net-brut-en.png'
import urlIllustrationNetBrut from 'Images/illustration-net-brut.png'
import React, { useContext } from 'react'
import { default as React, useContext } from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { Link } from 'react-router-dom'
export default function Salarié() {
const { t, i18n } = useTranslation()
const isEmbedded = React.useContext(IsEmbeddedContext)
return (
<>
@ -101,7 +103,8 @@ In addition to the salary, our simulator takes into account the calculation of b
There are deferred hiring aids that are not taken into account by our simulator, you can find them on the official portal.`
export let SalarySimulation = () => {
useSimulationConfig(salariéConfig)
const dispatch = useDispatch()
dispatch(setSimulationConfig(salariéConfig))
const sitePaths = useContext(SitePathsContext)
return (
<>

View File

@ -13,7 +13,7 @@ describe('bug-analyse-many', function() {
- nom: cotisations
formule:
somme:
- cotisation a [salarié]
- cotisation a .salarié
- cotisation b
- nom: cotisation a

View File

@ -1,4 +1,3 @@
import { expect } from 'chai'
// $FlowFixMe
import salariéConfig from 'Components/simulationConfigs/salarié.yaml'

View File

@ -140,28 +140,6 @@ describe('collectMissingVariables', function() {
expect(result).to.be.empty
})
it('should report missing variables in switch statements', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'11 > dix': '1000%',
'3 > dix': '1100%',
'1 > dix': '1200%'
}
}
},
{ nom: 'top . dix' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
expect(result).to.include('top . dix')
})
// TODO : enlever ce test, depuis que l'on évalue plus les branches qui ne sont pas encore applicable
it.skip('should report missing variables in variations', function() {
let rawRules = [
@ -217,75 +195,6 @@ describe('collectMissingVariables', function() {
// TODO
// expect(result).to.include('top . trois')
})
it('should not report missing variables in switch for consequences of false conditions', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'8 > 10': '1000%',
'1 > 2': 'dix'
}
}
},
{ nom: 'top . dix' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
expect(result).to.be.empty
})
it('should report missing variables in consequence when its condition is unresolved', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'10 > 11': '1000%',
'3 > dix': {
douze: '560%',
'1 > 2': '75015%'
}
}
}
},
{ nom: 'top . douze' },
{ nom: 'top . dix' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
expect(result).to.include('top . dix')
expect(result).to.include('top . douze')
})
it('should not report missing variables when a switch short-circuits', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'11 > 10': '1000%',
'3 > dix': '1100%',
'1 > dix': '1200%'
}
}
},
{ nom: 'top . dix' }
],
rules = parseAll(rawRules.map(enrichRule)),
analysis = analyse(rules, 'startHere')(stateSelector),
result = collectMissingVariables(analysis.targets)
expect(result).to.be.empty
})
})
describe('nextSteps', function() {
@ -368,9 +277,10 @@ describe('nextSteps', function() {
}[name])
let rules = parseAll(realRules.map(enrichRule)),
analysis = analyse(rules, 'contrat salarié . rémunération . net')(
stateSelector
),
analysis = analyse(
rules,
'contrat salarié . rémunération . net'
)(stateSelector),
result = collectMissingVariables(analysis.targets)
expect(result).to.include('contrat salarié . CDD . motif')

View File

@ -198,7 +198,7 @@ describe('inversions', () => {
taux: 50%
- nom: total
formule: cotisation [employeur] + cotisation [salarié]
formule: cotisation .employeur + cotisation .salarié
- nom: brut
unité:

View File

@ -32,7 +32,6 @@ describe('library', function() {
- nom: yo
formule: 1
- nom: ya
période: flexible
formule: contrat salarié . rémunération . net + yo
`

View File

@ -5,12 +5,12 @@
*/
import { expect } from 'chai'
import { serialiseUnit } from 'Engine/units'
import * as R from 'ramda'
import { collectMissingVariables } from '../source/engine/generateQuestions'
import { enrichRule } from '../source/engine/rules'
import { analyse, parseAll } from '../source/engine/traverse'
import { collectMissingVariables } from '../source/engine/generateQuestions'
import testSuites from './load-mecanism-tests'
import * as R from 'ramda'
import { serialiseUnit } from 'Engine/units'
describe('Mécanismes', () =>
testSuites.map(([suiteName, suite]) =>
@ -23,6 +23,7 @@ describe('Mécanismes', () =>
({
nom: testTexte,
situation,
'unités par défaut': defaultUnits,
'valeur attendue': valeur,
'variables manquantes': expectedMissing
}) =>
@ -38,7 +39,7 @@ describe('Mécanismes', () =>
),
state = situation || {},
stateSelector = name => state[name],
analysis = analyse(rules, test)(stateSelector),
analysis = analyse(rules, test, defaultUnits)(stateSelector),
missing = collectMissingVariables(analysis.targets),
target = analysis.targets[0]

View File

@ -1,77 +0,0 @@
# Utiliser http://romainvaleri.online.fr/ pour se donner des idées de noms de variables originales
- nom: dégradation mineure
- nom: dégradation majeure
- nom: retenue sur dépot de garantie
test: Aiguillage numérique simple
formule:
aiguillage numérique:
dégradation mineure: 10%
dégradation majeure: 30%
exemples:
- nom: le premier aiguillage est activé -> sa valeur est renvoyée
situation:
dégradation mineure: oui
valeur attendue: 0.1
- nom: seul le 2ème aiguillage est activé
situation:
dégradation mineure: non
dégradation majeure: oui
valeur attendue: 0.3
- nom: aucun aiguillage n'est activé
situation:
dégradation mineure: non
dégradation majeure: non
valeur attendue: 0
- nom: L'ordre des termes est important
situation:
dégradation mineure: null
dégradation majeure: oui
valeur attendue: null
- nom: montant caution
unité:
- test: Imbrication d'aiguillages numériques
formule:
aiguillage numérique:
dégradation mineure: 5%
dégradation majeure:
montant caution > 2000: 20%
montant caution > 1000: 10%
exemples:
- nom: imbrication simple
situation:
dégradation mineure: oui
dégradation majeure: non
montant caution: 3000
valeur attendue: 0.05
- nom: imbrication simple 2
situation:
dégradation mineure: non
dégradation majeure: oui
montant caution: 1200
valeur attendue: 0.10
- nom: imbrication nulle
valeur attendue: null
variables manquantes:
- montant caution
- dégradation mineure
- dégradation majeure
- nom: variables manquantes même si innaccessibles
situation:
dégradation mineure: non
valeur attendue: null
variables manquantes:
- montant caution
- dégradation majeure
# pouvoir tester les variables inconnues mais requises ?

View File

@ -1,126 +1,121 @@
- nom: montant
- nom: montant
unité:
- test: montant franchisé
unité:
formule:
allègement:
formule:
allègement:
assiette: montant
franchise: 1200
exemples:
- situation:
exemples:
- situation:
montant: 1000
valeur attendue: 0
- situation:
- situation:
valeur attendue: null
variables manquantes:
- montant
- test: montant décoté
unité:
formule:
allègement:
formule:
allègement:
assiette: montant
décote:
décote:
plafond: 2040
taux: 100%
exemples:
- situation:
exemples:
- situation:
montant: 1000
valeur attendue: 0
- test: montant franchisé et décoté
unité:
formule:
allègement:
formule:
allègement:
assiette: montant
franchise: 1200
décote:
décote:
plafond: 2040
taux: 75%
exemples:
- situation:
exemples:
- situation:
montant: 100
valeur attendue: 0
- situation:
- situation:
montant: 1200
valeur attendue: 570
- situation:
- situation:
montant: 1620
valeur attendue: 1305
- situation:
- situation:
montant: 2040
valeur attendue: 2040
- test: montant abattu
unité:
formule:
allègement:
formule:
allègement:
assiette: montant
abattement: 20507
exemples:
- situation:
exemples:
- situation:
montant: 10000
valeur attendue: 0
- situation:
- situation:
montant: 80000
valeur attendue: 59493
- test: montant abattu en pourcentage
unité:
formule:
allègement:
formule:
allègement:
assiette: montant
abattement: 15%
exemples:
- situation:
exemples:
- situation:
montant: 10000
valeur attendue: 8500
- situation:
- situation:
montant: 80000
valeur attendue: 68000
- test: montant abattu avec plafond numérique
unité:
formule:
allègement:
formule:
allègement:
assiette: montant
abattement: 15%
plafond: 12000
exemples:
- situation:
exemples:
- situation:
montant: 10000
valeur attendue: 8500
- situation:
- situation:
montant: 100000
valeur attendue: 88000 # 85000 s'il n'y avait pas de plafond à la somme abattue
- test: montant franchisé, décote, abattu
unité:
formule:
allègement:
formule:
allègement:
assiette: montant
franchise: 1200
décote:
décote:
plafond: 2040
taux: 75%
abattement: 20507
exemples:
- situation:
abattement: 20507
exemples:
- situation:
montant: 100
valeur attendue: 0
- situation:
- situation:
montant: 1620
valeur attendue: 0
- situation:
- situation:
montant: 3000
valeur attendue: 0
- situation:
- situation:
montant: 21000
valeur attendue: 493

View File

@ -1,5 +1,5 @@
- nom: base
unité: £
unité: £
formule: 300
- nom: assiette
@ -7,37 +7,36 @@
- test: Simple
formule:
barème continu:
barème continu:
assiette: assiette
multiplicateur: base
points:
points:
0: 0%
0.4: 3.16%
1.1: 6.35%
unité attendue: £
exemples:
exemples:
- nom: Premier point
situation:
situation:
assiette: 10
valeur attendue: 0.026
- nom: Deuxième point
situation:
situation:
assiette: 120
valeur attendue: 3.792
- nom: Premier point
situation:
- nom: Premier point
situation:
assiette: 150
valeur attendue: 5.423
- nom: Troisième point
situation:
- nom: Troisième point
situation:
assiette: 330
valeur attendue: 20.955
- nom: Au-delà
situation:
- nom: Au-delà
situation:
assiette: 1000
valeur attendue: 63.5
- nom: base deux
unité: µ
formule: 300
@ -46,6 +45,7 @@
unité: µ
- test: Retour de taux, pas d'assiette
unité: '%'
formule:
barème continu:
assiette: assiette deux
@ -56,24 +56,24 @@
1: 0%
retourne seulement le taux: oui
unité attendue: '%'
exemples:
exemples:
- nom: Premier point
situation:
situation:
assiette deux: 200
valeur attendue: 1
valeur attendue: 100
- nom: Deuxième point
situation:
situation:
assiette deux: 225
valeur attendue: 1
valeur attendue: 100
- nom: Troisième point
situation:
situation:
assiette deux: 262.5
valeur attendue: 0.5
valeur attendue: 50
- nom: Quatrième point
situation:
situation:
assiette deux: 300
valeur attendue: 0
- nom: Cinquième point
situation:
situation:
assiette deux: 300
valeur attendue: 0

View File

@ -78,34 +78,34 @@
assiette: assiette
multiplicateur: base
tranches:
- en-dessous de: 1
taux: taux variable
- au-dessus de: 1
taux: 90%
- en-dessous de: 1
taux: taux variable
- au-dessus de: 1
taux: 90%
unité attendue:
exemples:
- nom: taux faible
situation:
assiette: 200
base: 100
ma condition: oui
valeur attendue: 119
- nom: taux fort
situation:
assiette: 200
base: 100
ma condition: non
valeur attendue: 146
- nom: assiette manquante
situation:
base: 100
ma condition: oui
variables manquantes:
- assiette
- nom: condition manquante
situation:
base: 100
assiette: 400
variables manquantes:
- ma condition
- nom: taux faible
situation:
assiette: 200
base: 100
ma condition: oui
valeur attendue: 119
- nom: taux fort
situation:
assiette: 200
base: 100
ma condition: non
valeur attendue: 146
- nom: assiette manquante
situation:
base: 100
ma condition: oui
variables manquantes:
- assiette
- nom: condition manquante
situation:
assiette: 40
base: 100
variables manquantes:
- ma condition

View File

@ -0,0 +1,236 @@
# This is not a mecanism test, but we make use of the simplicity of declaring tests in YAML, only available for mecanisms for now
- nom: douches par mois
question: Combien prenez-vous de douches par mois ?
unité: douche/mois
- test: Conversion de reference
formule: douches par mois [douche/an]
exemples:
- situation:
douches par mois: 30
valeur attendue: 360
- test: Conversion de reference 2
unité: douche/an
formule: douches par mois
exemples:
- situation:
douches par mois: 30
valeur attendue: 360
- nom: Unité de variable prioritaire devant les unités par défaut
situation:
douches par mois: 30
unités par défaut: [douche/mois]
valeur attendue: 360
- test: Conversion de variable
formule: 1.5 kCo2/douche * douches par mois
exemples:
- situation:
douches par mois: 30
valeur attendue: 45
unité attendue: kCo2/mois
- nom: Unité cible de simulation
situation:
douches par mois: 20
unités par défaut: [kCo2/an]
unité attendue: kCo2/an
valeur attendue: 360
- test: Conversion de variable et expressions
unité: kCo2/an
formule: 1 kCo2/douche * 10 douche/mois
exemples:
- valeur attendue: 120
- test: Conversion de pourcentage
unité: €/an
formule: 1000€ * 1% /mois
exemples:
- valeur attendue: 120
- test: Conversion en pourcentage
unité: '%'
formule: 28h / 35h
exemples:
- valeur attendue: 80
- test: Conversion dans un mécanisme
unité: €/an
formule:
le minimum de:
- 100 €/mois
- 1120 €/an
exemples:
- valeur attendue: 1120
- nom: assiette mensuelle
unité: €/mois
- test: Conversion de mécanisme 1
unité: €/an
formule:
barème:
assiette: assiette mensuelle [€/an]
tranches:
- en-dessous de: 30000
taux: 4.65%
- de: 30000
à: 90000
taux: 3%
- au-dessus de: 90000
taux: 1%
exemples:
- situation:
assiette mensuelle: 3000
valeur attendue: 1575
- nom: assiette annuelle
unité: €/an
- test: Conversion de mécanisme 2
formule:
barème:
assiette: assiette annuelle [€/mois]
tranches:
- en-dessous de: 2500
taux: 4.65%
- de: 2500
à: 7500
taux: 3%
- au-dessus de: 7500
taux: 1%
exemples:
- situation:
assiette annuelle: 36000
valeur attendue: 131.25
unités par défaut: [€/mois]
- test: Conversion dans une expression
unité: €/an
formule: 80 €/mois + 1120 €/an + 20 €/mois
exemples:
- valeur attendue: 2320
- test: Conversion dans une comparaison
formule: 100€/mois = 1.2k€/an
exemples:
- valeur attendue: true
- nom: mutuelle
formule: 30 €/mois
- nom: retraite
formule:
multiplication:
assiette: assiette annuelle
plafond: 12 k€/an
taux: 10%
- test: Conversion dans une somme compliquée
formule:
somme:
- mutuelle
- retraite
exemples:
- situation:
assiette annuelle: 20000
unités par défaut: [€/mois]
valeur attendue: 130
- nom: maladie
formule:
multiplication:
assiette: assiette annuelle
composantes:
- attributs:
dû par: employeur
taux: 15%
- attributs:
dû par: salarié
taux: 5%
plafond: 1000 €/mois
- test: Conversion avec composantes
unité: €/mois
formule:
somme:
- maladie .salarié
- retraite
- mutuelle
exemples:
- situation:
assiette annuelle: 20000
valeur attendue: 180
- test: Conversion dans un allègement
formule:
allègement:
assiette: 1000€/an
abattement: 10€/mois
exemples:
- unités par défaut: [€/an]
valeur attendue: 880
- test: Conversion dans avec un abattement en %
unité par défaut: €/an
formule:
allègement:
assiette: 1000€/an
abattement: 10%
exemples:
- valeur attendue: 900
- nom: assiette cotisations
formule:
allègement:
assiette: assiette mensuelle
abattement: 1200 €/an
- nom: prévoyance cadre
formule:
multiplication:
assiette: assiette cotisations
taux: 1.5%
- test: Conversion avec plusieurs échelons
formule:
somme:
- prévoyance cadre
- 35€/mois
exemples:
- unités par défaut: [€/an]
situation:
assiette mensuelle: 1100
valeur attendue: 600
- test: Conversion de situation
formule:
somme:
- retraite
- mutuelle
exemples:
- unités par défaut: [€/an]
situation:
retraite: 4000
valeur attendue: 4360
- nom: rémunération brute
unité par défaut: €/mois
- test: Conversion de situation avec unité
unité: €/an
formule:
multiplication:
assiette: rémunération brute
taux: 10%
exemples:
- situation:
rémunération brute: 1000
valeur attendue: 1200
- unités par défaut: [k€/an]
situation:
rémunération brute: 12
valeur attendue: 1200

View File

@ -52,12 +52,13 @@
unité: '%'
- test: soustraction
formule: 1 - taux
unité: '%'
formule: 100% - taux
unité attendue: '%'
exemples:
- situation:
taux: 0.89
valeur attendue: 0.11
taux: 89
valeur attendue: 11
- test: addition
formule: salaire de base + 2000
@ -137,6 +138,7 @@
valeur attendue: false
- nom: plafond sécurité sociale
unité: $
- nom: CDD
@ -218,16 +220,14 @@
valeur attendue: false
- nom: revenu
période: mois
unité:
unité: €/mois
- test: variable modifiée temporellement
formule: revenu [annuel]
période: aucune
- test: unité de variable modifiée
formule: revenu [k€/an]
exemples:
- situation:
revenu: 1000
valeur attendue: 12000
valeur attendue: 12
- test: opérations multiples
formule: 4 * plafond sécurité sociale * 10%

View File

@ -27,7 +27,6 @@
situation:
valeur attendue: 9.9
- nom: mon plafond
unité:
@ -66,8 +65,6 @@
mon facteur: 3
valeur attendue: 300
- test: Multiplication complète
formule:
multiplication:
@ -76,7 +73,7 @@
plafond: mon plafond
taux: 0.5%
unité attendue: -patates
unité attendue: .patates
exemples:
- nom:
situation:
@ -84,8 +81,6 @@
mon facteur: 2
mon plafond: 100
valeur attendue: 1
# This should work, but with the use of objectShape & co, the short circuits are not performed
#- test: Multiplication complète
# formule:
@ -103,4 +98,3 @@
# valeur attendue: 0
# variables manquantes: []

View File

@ -1,157 +0,0 @@
# This is not a mecanism test, but we make use of the simplicity of declaring tests in YAML, only available for mecanisms for now
- nom: nombre de douches
période: mois
question: Combien prenez-vous de douches par mois ?
unité: _
suggestions:
- 30
- test: impact des douches
période: année
formule: 1 * nombre de douches
exemples:
- situation:
nombre de douches: 30
valeur attendue: 360
- nom: impact par douche
formule: 1
unité: kgCO2e
- test: impact des douches erroné
période: année
formule: impact par douche * nombre de douches
exemples:
- situation:
nombre de douches: 30
valeur attendue: 360
- nom: assiette mensuelle
période: mois
unité:
- test: Périodes, barème annuel assiette mensuelle
période: année
formule:
barème:
# cette formule appellant l'assiette est annuelle :
# si l'assiette est aussi annuelle dans le contexte de la simulation actuelle, c'est bon
# sinon une conversion est nécessaire et faite automatiquement par le moteur
assiette: assiette mensuelle
tranches:
# ce sont ces chiffres là qui imposent à la règle d'être annuelle
# de plus, les règles annuelles de la loi sont rarement traduites officiellement en d'autres périodes
- en-dessous de: 30000
taux: 4.65%
- de: 30000
à: 90000
taux: 3%
- au-dessus de: 90000
taux: 1%
exemples:
- situation:
assiette mensuelle: 3000
valeur attendue: 1575
- nom: assiette annuelle
période: année
unité:
- test: Périodes, barème mensuel assiette annuelle
période: mois
formule:
barème:
# cette formule appellant l'assiette est annuelle :
# si l'assiette est aussi annuelle dans le contexte de la simulation actuelle, c'est bon
# sinon une conversion est nécessaire et faite automatiquement par le moteur
assiette: assiette annuelle
tranches:
# ce sont ces chiffres là qui imposent à la règle d'être annuelle
# de plus, les règles annuelles de la loi sont rarement traduites officiellement en d'autres périodes
- en-dessous de: 2500
taux: 4.65%
- de: 2500
à: 7500
taux: 3%
- au-dessus de: 7500
taux: 1%
exemples:
- situation:
assiette annuelle: 36000
valeur attendue: 131.25
- nom: assiette
période: flexible
unité:
- test: Périodes, période dans la situation
période: année
formule:
barème:
assiette: assiette
tranches:
- en-dessous de: 30000
taux: 4.65%
- de: 30000
à: 90000
taux: 3%
- au-dessus de: 90000
taux: 1%
exemples:
- situation:
période: mois
assiette: 3000
valeur attendue: 1575
- situation:
période: année
assiette: 36000
valeur attendue: 1575
- nom: assiette deux
période: mois
unité:
- test: Périodes, variable neutre appelant variable mensuelle
période: flexible
formule:
multiplication:
assiette: assiette deux
taux: 10%
exemples:
- situation:
période: mois
assiette deux: 3000
valeur attendue: 300
- nom: assiette trois
période: année
unité:
- test: Périodes, variable neutre appelant variable annuelle
période: flexible
formule:
multiplication:
assiette: assiette trois
taux: 10%
exemples:
- situation:
période: mois
assiette trois: 36000
valeur attendue: 300
- test: Périodes, préfixe de modification temporelle
formule: assiette trois [mensuel]
exemples:
- situation:
assiette trois: 12000
valeur attendue: 1000

View File

@ -18,7 +18,7 @@
applicable si: client enfant
remplace:
règle: prix du repas
par: 8
par: 8 /repas
- test: modifie une règle
formule: restaurant . prix du repas
@ -40,8 +40,8 @@
- nom: cotisations
formule:
somme:
- retraite [salarié]
- retraite [employeur]
- retraite .salarié
- retraite .employeur
- chômage
- maladie
@ -140,7 +140,7 @@
formule: cotisations
remplace:
- règle: cotisations . chômage
par: 10
par: 10
- règle: cotisations . maladie
par: 0
exemples:

View File

@ -1,8 +1,8 @@
import { AssertionError } from 'chai'
import { merge } from 'ramda'
import { exampleAnalysisSelector } from 'Selectors/analyseSelectors'
import { rules } from '../source/engine/rules'
import { parseAll } from '../source/engine/traverse'
import { exampleAnalysisSelector } from 'Selectors/analyseSelectors'
import { merge } from 'ramda'
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
@ -13,7 +13,8 @@ let runExamples = (examples, rule) =>
rules,
currentExample: {
situation: ex.situation,
dottedName: rule.dottedName
dottedName: rule.dottedName,
defaultUnits: ex['unités par défaut']
}
},
{ dottedName: rule.dottedName }

View File

@ -1,299 +1,265 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`calculate simulations-artiste-auteur: bnc 1`] = `"[1230]"`;
exports[`calculate simulations-artiste-auteur: bnc 1`] = `"[1230]"`;
exports[`calculate simulations-artiste-auteur: bnc 2`] = `"[1863]"`;
exports[`calculate simulations-artiste-auteur: bnc 2`] = `"[1230]"`;
exports[`calculate simulations-artiste-auteur: bnc 3`] = `"[932]"`;
exports[`calculate simulations-artiste-auteur: bnc 3`] = `"[1230]"`;
exports[`calculate simulations-artiste-auteur: salarié 1`] = `"[160]"`;
exports[`calculate simulations-artiste-auteur: salarié 1`] = `"[160]"`;
exports[`calculate simulations-artiste-auteur: salarié 2`] = `"[1603]"`;
exports[`calculate simulations-artiste-auteur: salarié 2`] = `"[1603]"`;
exports[`calculate simulations-artiste-auteur: salarié 3`] = `"[12372]"`;
exports[`calculate simulations-artiste-auteur: salarié 3`] = `"[12372]"`;
exports[`calculate simulations-auto-entrepreneur: aides 1`] = `"[5299,299,5000,0,5000]"`;
exports[`calculate simulations-auto-entrepreneur: aides 1`] = `"[5299,299,5000,0,5000]"`;
exports[`calculate simulations-auto-entrepreneur: aides 2`] = `"[52991,2991,50000,2314,47686]"`;
exports[`calculate simulations-auto-entrepreneur: aides 2`] = `"[52991,2991,50000,2314,47686]"`;
exports[`calculate simulations-auto-entrepreneur: impôt sur le revenu 1`] = `"[32092,7092,25000,706,24294]"`;
exports[`calculate simulations-auto-entrepreneur: impôt sur le revenu 1`] = `"[32092,7092,25000,706,24294]"`;
exports[`calculate simulations-auto-entrepreneur: périodes 1`] = `"[128,28,100,0,100]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 1`] = `"[642,142,500,0,500]"`;
exports[`calculate simulations-auto-entrepreneur: périodes 2`] = `"[642,142,500,0,500]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 2`] = `"[1284,284,1000,0,1000]"`;
exports[`calculate simulations-auto-entrepreneur: périodes 3`] = `"[1284,284,1000,0,1000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 3`] = `"[2569,569,2000,0,2000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 1`] = `"[642,142,500,0,500]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 4`] = `"[6422,1422,5000,0,5000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 2`] = `"[1284,284,1000,0,1000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 5`] = `"[12844,2844,10000,0,10000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 3`] = `"[2569,569,2000,0,2000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 6`] = `"[25688,5688,20000,0,20000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 4`] = `"[6422,1422,5000,0,5000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 7`] = `"[64221,14221,50000,3835,46165]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 5`] = `"[12844,2844,10000,0,10000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 8`] = `"[89910,19910,70000,7688,62312]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 6`] = `"[25688,5688,20000,0,20000]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 9`] = `"[128442,28442,100000,13468,86532]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 7`] = `"[64221,14221,50000,3835,46165]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 10`] = `"[1284423,284423,1000000,282020,717980]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 8`] = `"[89910,19910,70000,7688,62312]"`;
exports[`calculate simulations-indépendant: acre 1`] = `"[73015,23015,50000,51980,8237,41763,null,73015]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 9`] = `"[128442,28442,100000,13468,86532]"`;
exports[`calculate simulations-indépendant: activité 1`] = `"[29091,9091,20000,20787,947,19053,null,29091]"`;
exports[`calculate simulations-auto-entrepreneur: échelle de revenus 10`] = `"[1284423,284423,1000000,282020,717980]"`;
exports[`calculate simulations-indépendant: activité 2`] = `"[29108,9108,20000,20787,947,19053,null,29108]"`;
exports[`calculate simulations-indépendant: acre 1`] = `"[73015,23015,50000,51980,8237,41763,null,73015]"`;
exports[`calculate simulations-indépendant: impôt sur le revenu 1`] = `"[29091,9091,20000,20787,728,19272,null,29091]"`;
exports[`calculate simulations-indépendant: activité 1`] = `"[29091,9091,20000,20787,947,19053,null,29091]"`;
exports[`calculate simulations-indépendant: impôt sur le revenu 2`] = `"[73015,23015,50000,51980,8317,41683,null,73015]"`;
exports[`calculate simulations-indépendant: activité 2`] = `"[29108,9108,20000,20787,947,19053,null,29108]"`;
exports[`calculate simulations-indépendant: impôt sur le revenu 3`] = `"[29091,9091,20000,20787,2079,17921,null,29091]"`;
exports[`calculate simulations-indépendant: impôt sur le revenu 1`] = `"[29091,9091,20000,20787,728,19272,null,29091]"`;
exports[`calculate simulations-indépendant: inversions 1`] = `"[2000,1369,631,683,0,631,null,2000]"`;
exports[`calculate simulations-indépendant: impôt sur le revenu 2`] = `"[73015,23015,50000,51980,8317,41683,null,73015]"`;
exports[`calculate simulations-indépendant: inversions 2`] = `"[50000,16017,33983,35338,3743,30240,null,50000]"`;
exports[`calculate simulations-indépendant: impôt sur le revenu 3`] = `"[29091,9091,20000,20787,2079,17921,null,29091]"`;
exports[`calculate simulations-indépendant: inversions 3`] = `"[14592,4592,10000,10393,0,10000,null,14592]"`;
exports[`calculate simulations-indépendant: inversions 1`] = `"[2000,1369,631,683,0,631,null,2000]"`;
exports[`calculate simulations-indépendant: inversions 4`] = `"[88759,27318,61441,63848,11441,50000,null,88759]"`;
exports[`calculate simulations-indépendant: inversions 2`] = `"[50000,16017,33983,35338,3743,30240,null,50000]"`;
exports[`calculate simulations-indépendant: inversions 5`] = `"[14592,4592,10000,10393,0,10000,null,15592]"`;
exports[`calculate simulations-indépendant: inversions 3`] = `"[14592,4592,10000,10393,0,10000,null,14592]"`;
exports[`calculate simulations-indépendant: inversions 6`] = `"[19000,5926,13074,13588,0,13074,1000,20000]"`;
exports[`calculate simulations-indépendant: inversions 4`] = `"[88759,27318,61441,63848,11441,50000,null,88759]"`;
exports[`calculate simulations-indépendant: inversions 7`] = `"[18000,5623,12377,12863,0,12377,2000,20000]"`;
exports[`calculate simulations-indépendant: inversions 5`] = `"[14592,4592,10000,10393,0,10000,null,15592]"`;
exports[`calculate simulations-indépendant: échelle de revenus 1`] = `"[1840,1340,500,547,0,500,null,1840]"`;
exports[`calculate simulations-indépendant: inversions 6`] = `"[19000,5926,13074,13588,0,13074,1000,20000]"`;
exports[`calculate simulations-indépendant: échelle de revenus 2`] = `"[2448,1448,1000,1064,0,1000,null,2448]"`;
exports[`calculate simulations-indépendant: inversions 7`] = `"[18000,5623,12377,12863,0,12377,2000,20000]"`;
exports[`calculate simulations-indépendant: échelle de revenus 3`] = `"[3056,1556,1500,1580,0,1500,null,3056]"`;
exports[`calculate simulations-indépendant: période 1`] = `"[1455,455,1000,1039,0,1000,null,1455]"`;
exports[`calculate simulations-indépendant: échelle de revenus 4`] = `"[3664,1664,2000,2097,0,2000,null,3664]"`;
exports[`calculate simulations-indépendant: période 2`] = `"[7239,2239,5000,5196,920,4080,null,7239]"`;
exports[`calculate simulations-indépendant: échelle de revenus 5`] = `"[7423,2423,5000,5199,0,5000,null,7423]"`;
exports[`calculate simulations-indépendant: échelle de revenus 1`] = `"[1840,1340,500,547,0,500,null,1840]"`;
exports[`calculate simulations-indépendant: échelle de revenus 6`] = `"[14592,4592,10000,10393,0,10000,null,14592]"`;
exports[`calculate simulations-indépendant: échelle de revenus 2`] = `"[2448,1448,1000,1064,0,1000,null,2448]"`;
exports[`calculate simulations-indépendant: échelle de revenus 7`] = `"[139472,39472,100000,103784,24383,75617,null,139472]"`;
exports[`calculate simulations-indépendant: échelle de revenus 3`] = `"[3056,1556,1500,1580,0,1500,null,3056]"`;
exports[`calculate simulations-indépendant: échelle de revenus 8`] = `"[1239593,239593,1000000,1033657,467702,532298,null,1239593]"`;
exports[`calculate simulations-indépendant: échelle de revenus 4`] = `"[3664,1664,2000,2097,0,2000,null,3664]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 1`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-indépendant: échelle de revenus 5`] = `"[7423,2423,5000,5199,0,5000,null,7423]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 2`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-indépendant: échelle de revenus 6`] = `"[14592,4592,10000,10393,0,10000,null,14592]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 3`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-indépendant: échelle de revenus 7`] = `"[139472,39472,100000,103784,24383,75617,null,139472]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 4`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-indépendant: échelle de revenus 8`] = `"[1239593,239593,1000000,1033657,467702,532298,null,1239593]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 5`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 1`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - avec charges 1`] = `"[5291,5291,5306,4,10,12]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 2`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - avec charges 2`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 3`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 1`] = `"[169,169,139,0,1,1]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 4`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 2`] = `"[738,738,323,0,2,2]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - activités 5`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 3`] = `"[2446,2446,2588,2,5,6]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - avec charges 1`] = `"[5291,5291,5306,4,10,12]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 4`] = `"[5291,5291,5306,4,10,12]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - avec charges 2`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 5`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - périodes 1`] = `"[80,80,98,1,2,3]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 6`] = `"[25686,28055,27050,4,45,59]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - périodes 2`] = `"[251,251,261,2,6,7]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 7`] = `"[46640,57031,52655,4,45,119]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - périodes 3`] = `"[2485,2808,2693,4,45,71]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 1`] = `"[15580,15580,6600,4,18,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 1`] = `"[169,169,139,0,1,1]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 2`] = `"[15560,15560,0,4,18,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 2`] = `"[738,738,323,0,2,2]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 3`] = `"[15444,15444,7047,4,14,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 3`] = `"[2446,2446,2588,2,5,6]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 4`] = `"[17417,17417,4093,3,8,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 4`] = `"[5291,5291,5306,4,10,12]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 5`] = `"[17417,17417,4093,3,8,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 5`] = `"[10982,10982,10742,4,19,23]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - avec charges 1`] = `"[7343,7343,4228,3,8,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 6`] = `"[25686,28055,27050,4,45,59]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - avec charges 2`] = `"[11599,12250,12332,4,24,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 7`] = `"[46640,57031,52655,4,45,119]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 1`] = `"[779,779,102,0,0,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 1`] = `"[15580,15580,6600,4,18,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 2`] = `"[1557,1557,205,0,0,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 2`] = `"[15560,15560,0,4,18,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 3`] = `"[3893,3893,1762,2,0,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 3`] = `"[15444,15444,7047,4,14,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 4`] = `"[7786,7786,3523,3,7,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 4`] = `"[17417,17417,4093,3,8,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 5`] = `"[15571,15571,7047,4,14,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 5`] = `"[17417,17417,4093,3,8,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 6`] = `"[36823,38928,17617,4,34,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - avec charges 1`] = `"[7343,7343,4228,3,8,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 7`] = `"[68654,77856,30496,4,56,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - avec charges 2`] = `"[11599,12250,12332,4,24,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 1`] = `"[13772,13772,10086,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - périodes 1`] = `"[156,156,20,0,0,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 2`] = `"[14571,14571,0,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - périodes 2`] = `"[389,389,176,2,0,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 3`] = `"[13761,13761,10077,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - périodes 3`] = `"[3626,3893,1762,4,41,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 4`] = `"[13772,13772,10086,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 1`] = `"[779,779,102,0,0,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 5`] = `"[13772,13772,10086,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 2`] = `"[1557,1557,205,0,0,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - avec charges 1`] = `"[6797,6797,4979,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 3`] = `"[3893,3893,1762,2,0,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - avec charges 2`] = `"[13772,13772,10086,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 4`] = `"[7786,7786,3523,3,7,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 1`] = `"[0,0,36807,3,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 5`] = `"[15571,15571,7047,4,14,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 2`] = `"[631,631,481,3,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 6`] = `"[36823,38928,17617,4,34,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 3`] = `"[3100,3100,2278,3,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 7`] = `"[68654,77856,30496,4,56,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 4`] = `"[6797,6797,4979,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 1`] = `"[13772,13772,10085,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 5`] = `"[13772,13772,10086,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 2`] = `"[14571,14571,0,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 6`] = `"[30240,33983,24902,4,48,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 3`] = `"[13761,13761,10077,4,21,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 7`] = `"[56157,69988,36158,4,56,0]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 4`] = `"[13772,13772,10085,4,21,0]"`;
exports[`calculate simulations-salarié: aides 1`] = `"[2302,0,0,2000,1561,1503]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 5`] = `"[13772,13772,10085,4,21,0]"`;
exports[`calculate simulations-salarié: aides 2`] = `"[12823,0,0,10000,8910,7652]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - avec charges 1`] = `"[6797,6797,4979,4,21,0]"`;
exports[`calculate simulations-salarié: apprentissage 1`] = `"[1551,0,0,1500,1446,1446]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - avec charges 2`] = `"[13772,13772,10085,4,21,0]"`;
exports[`calculate simulations-salarié: apprentissage 2`] = `"[1384,167,0,1500,1446,1446]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - périodes 1`] = `"[80,80,60,3,21,0]"`;
exports[`calculate simulations-salarié: assimilé salarié 1`] = `"[7014,0,0,5000,3943,3304]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - périodes 2`] = `"[327,327,240,3,21,0]"`;
exports[`calculate simulations-salarié: assimilé salarié 2`] = `"[1583,0,0,1500,1163,1163]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - périodes 3`] = `"[2927,3397,2422,4,56,0]"`;
exports[`calculate simulations-salarié: assimilé salarié 3`] = `"[3742,0,0,3000,2348,2150]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 1`] = `"[0,0,36807,3,21,0]"`;
exports[`calculate simulations-salarié: atmp 1`] = `"[2549,0,0,2000,1561,1503]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 2`] = `"[631,631,481,3,21,0]"`;
exports[`calculate simulations-salarié: avantages 1`] = `"[2682,0,0,2000,1540,1464]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 3`] = `"[3100,3100,2278,3,21,0]"`;
exports[`calculate simulations-salarié: avantages 2`] = `"[2692,0,0,2000,1539,1462]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 4`] = `"[6797,6797,4979,4,21,0]"`;
exports[`calculate simulations-salarié: avantages 3`] = `"[2602,0,0,2000,1549,1481]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 5`] = `"[13772,13772,10085,4,21,0]"`;
exports[`calculate simulations-salarié: cadre 1`] = `"[4122,0,0,3000,2348,2149]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 6`] = `"[30240,33983,24902,4,48,0]"`;
exports[`calculate simulations-salarié: cdd 1`] = `"[2514,0,0,2000,1561,1503]"`;
exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 7`] = `"[56157,69988,36158,4,56,0]"`;
exports[`calculate simulations-salarié: cdd 2`] = `"[2605,0,0,2000,1599,1532]"`;
exports[`calculate simulations-salarié: aides 1`] = `"[2302,0,0,2000,1561,1503]"`;
exports[`calculate simulations-salarié: heures supplémentaires 1`] = `"[2599,0,0,2000,1636,1578]"`;
exports[`calculate simulations-salarié: aides 2`] = `"[12823,0,0,10000,8910,7652]"`;
exports[`calculate simulations-salarié: heures supplémentaires 2`] = `"[3123,0,0,2000,2009,1940]"`;
exports[`calculate simulations-salarié: apprentissage 1`] = `"[1551,0,0,1500,1446,1446]"`;
exports[`calculate simulations-salarié: heures supplémentaires 3`] = `"[2669,0,0,2000,1636,1578]"`;
exports[`calculate simulations-salarié: apprentissage 2`] = `"[1384,167,0,1500,1446,1446]"`;
exports[`calculate simulations-salarié: heures supplémentaires 4`] = `"[2580,0,0,2000,1627,1569]"`;
exports[`calculate simulations-salarié: assimilé salarié 1`] = `"[7014,0,0,5000,3943,3304]"`;
exports[`calculate simulations-salarié: heures supplémentaires 5`] = `"[3043,0,0,2000,1970,1911]"`;
exports[`calculate simulations-salarié: assimilé salarié 2`] = `"[1583,0,0,1500,1163,1163]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 1`] = `"[4076,0,0,3000,2353,2168]"`;
exports[`calculate simulations-salarié: assimilé salarié 3`] = `"[3742,0,0,3000,2348,2150]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 2`] = `"[41765,0,0,30000,24267,14611]"`;
exports[`calculate simulations-salarié: atmp 1`] = `"[2549,0,0,2000,1561,1503]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 3`] = `"[4106,0,0,3000,2353,2172]"`;
exports[`calculate simulations-salarié: avantages 1`] = `"[2682,0,0,2000,1540,1464]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 4`] = `"[3915,0,0,3000,2353,2205]"`;
exports[`calculate simulations-salarié: avantages 2`] = `"[2692,0,0,2000,1539,1462]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 5`] = `"[41765,0,0,30000,24267,14611]"`;
exports[`calculate simulations-salarié: avantages 3`] = `"[2602,0,0,2000,1549,1481]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 6`] = `"[4076,0,0,3000,2353,2242]"`;
exports[`calculate simulations-salarié: cadre 1`] = `"[4122,0,0,3000,2348,2149]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 7`] = `"[41765,0,0,30000,24267,15869]"`;
exports[`calculate simulations-salarié: cdd 1`] = `"[2514,0,0,2000,1561,1503]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 8`] = `"[4076,0,0,3000,2353,2107]"`;
exports[`calculate simulations-salarié: cdd 2`] = `"[2605,0,0,2000,1599,1532]"`;
exports[`calculate simulations-salarié: inversions 1`] = `"[2000,0,0,1738,1354,1343]"`;
exports[`calculate simulations-salarié: heures supplémentaires 1`] = `"[2599,0,0,2000,1636,1578]"`;
exports[`calculate simulations-salarié: inversions 2`] = `"[3474,0,0,2554,2000,1852]"`;
exports[`calculate simulations-salarié: heures supplémentaires 2`] = `"[3123,0,0,2000,2009,1940]"`;
exports[`calculate simulations-salarié: inversions 3`] = `"[3764,0,0,2769,2170,2000]"`;
exports[`calculate simulations-salarié: heures supplémentaires 3`] = `"[2669,0,0,2000,1636,1578]"`;
exports[`calculate simulations-salarié: stage 1`] = `"[507,0,0,500,500,500]"`;
exports[`calculate simulations-salarié: heures supplémentaires 4`] = `"[2580,0,0,2000,1627,1569]"`;
exports[`calculate simulations-salarié: stage 2`] = `"[2493,0,0,2000,1749,1749]"`;
exports[`calculate simulations-salarié: heures supplémentaires 5`] = `"[3043,0,0,2000,1970,1911]"`;
exports[`calculate simulations-salarié: temps partiel 1`] = `"[2605,0,2188,2000,1561,1503]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 1`] = `"[4076,0,0,3000,2353,2168]"`;
exports[`calculate simulations-salarié: temps partiel 2`] = `"[2533,0,2500,1857,1448,1416]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 2`] = `"[41765,0,0,30000,24267,14656]"`;
exports[`calculate simulations-salarié: échelle de salaires 1`] = `"[130,0,0,100,57,57]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 3`] = `"[4106,0,0,3000,2353,2270]"`;
exports[`calculate simulations-salarié: échelle de salaires 2`] = `"[284,0,0,250,176,176]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 4`] = `"[3915,0,0,3000,2353,2205]"`;
exports[`calculate simulations-salarié: échelle de salaires 3`] = `"[541,0,0,500,374,374]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 5`] = `"[41765,0,0,30000,24267,14656]"`;
exports[`calculate simulations-salarié: échelle de salaires 4`] = `"[798,0,0,750,572,572]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 6`] = `"[4076,0,0,3000,2353,2242]"`;
exports[`calculate simulations-salarié: échelle de salaires 5`] = `"[1055,0,0,1000,770,770]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 7`] = `"[41765,0,0,30000,24267,15913]"`;
exports[`calculate simulations-salarié: échelle de salaires 6`] = `"[1312,0,0,1250,968,968]"`;
exports[`calculate simulations-salarié: impôt sur le revenu 8`] = `"[4076,0,0,3000,2353,2107]"`;
exports[`calculate simulations-salarié: échelle de salaires 7`] = `"[1569,0,0,1500,1165,1165]"`;
exports[`calculate simulations-salarié: inversions 1`] = `"[2000,0,0,1738,1354,1343]"`;
exports[`calculate simulations-salarié: échelle de salaires 8`] = `"[2494,0,0,2000,1561,1503]"`;
exports[`calculate simulations-salarié: inversions 2`] = `"[3474,0,0,2554,2000,1852]"`;
exports[`calculate simulations-salarié: échelle de salaires 9`] = `"[3401,0,0,2500,1957,1815]"`;
exports[`calculate simulations-salarié: inversions 3`] = `"[3764,0,0,2769,2170,2000]"`;
exports[`calculate simulations-salarié: échelle de salaires 10`] = `"[4076,0,0,3000,2353,2159]"`;
exports[`calculate simulations-salarié: périodes 1`] = `"[3405,0,0,3000,2112,2112]"`;
exports[`calculate simulations-salarié: échelle de salaires 11`] = `"[5674,0,0,4000,3146,2744]"`;
exports[`calculate simulations-salarié: périodes 2`] = `"[61150,0,0,45000,35349,31190]"`;
exports[`calculate simulations-salarié: échelle de salaires 12`] = `"[7085,0,0,5000,3948,3321]"`;
exports[`calculate simulations-salarié: périodes 3`] = `"[674660,0,0,500000,417064,243499]"`;
exports[`calculate simulations-salarié: échelle de salaires 13`] = `"[14319,0,0,10000,7959,6069]"`;
exports[`calculate simulations-salarié: stage 1`] = `"[507,0,0,500,500,500]"`;
exports[`calculate simulations-salarié: échelle de salaires 14`] = `"[28336,0,0,20000,15969,10665]"`;
exports[`calculate simulations-salarié: stage 2`] = `"[2493,0,0,2000,1749,1749]"`;
exports[`calculate simulations-salarié: échelle de salaires 15`] = `"[128506,0,0,100000,87197,46275]"`;
exports[`calculate simulations-salarié: temps partiel 1`] = `"[2605,0,2188,2000,1561,1503]"`;
exports[`calculate simulations-salarié: temps partiel 2`] = `"[2533,0,2500,1857,1448,1416]"`;
exports[`calculate simulations-salarié: échelle de salaires 1`] = `"[130,0,0,100,57,57]"`;
exports[`calculate simulations-salarié: échelle de salaires 2`] = `"[284,0,0,250,176,176]"`;
exports[`calculate simulations-salarié: échelle de salaires 3`] = `"[541,0,0,500,374,374]"`;
exports[`calculate simulations-salarié: échelle de salaires 4`] = `"[798,0,0,750,572,572]"`;
exports[`calculate simulations-salarié: échelle de salaires 5`] = `"[1055,0,0,1000,770,770]"`;
exports[`calculate simulations-salarié: échelle de salaires 6`] = `"[1312,0,0,1250,968,968]"`;
exports[`calculate simulations-salarié: échelle de salaires 7`] = `"[1569,0,0,1500,1165,1165]"`;
exports[`calculate simulations-salarié: échelle de salaires 8`] = `"[2494,0,0,2000,1561,1503]"`;
exports[`calculate simulations-salarié: échelle de salaires 9`] = `"[3401,0,0,2500,1957,1815]"`;
exports[`calculate simulations-salarié: échelle de salaires 10`] = `"[4076,0,0,3000,2353,2159]"`;
exports[`calculate simulations-salarié: échelle de salaires 11`] = `"[5674,0,0,4000,3146,2744]"`;
exports[`calculate simulations-salarié: échelle de salaires 12`] = `"[7085,0,0,5000,3948,3321]"`;
exports[`calculate simulations-salarié: échelle de salaires 13`] = `"[14319,0,0,10000,7959,6069]"`;
exports[`calculate simulations-salarié: échelle de salaires 14`] = `"[28336,0,0,20000,15969,10941]"`;
exports[`calculate simulations-salarié: échelle de salaires 15`] = `"[128506,0,0,100000,87197,50180]"`;
exports[`calculate simulations-salarié: échelle de salaires 16`] = `"[1243750,0,0,1000000,896297,451743]"`;
exports[`calculate simulations-salarié: échelle de salaires 16`] = `"[1243750,0,0,1000000,896297,446127]"`;

View File

@ -10,14 +10,6 @@
- dirigeant . auto-entrepreneur . revenu net de cotisations: 100000
- dirigeant . auto-entrepreneur . revenu net de cotisations: 1000000
périodes:
- dirigeant . auto-entrepreneur . revenu net de cotisations: 100
période: mois
- dirigeant . auto-entrepreneur . revenu net de cotisations: 500
période: mois
- dirigeant . auto-entrepreneur . revenu net de cotisations: 1000
période: mois
aides:
- dirigeant . auto-entrepreneur . revenu net de cotisations: 5000
entreprise . ACRE: true

View File

@ -8,12 +8,6 @@
- dirigeant . indépendant . revenu net de cotisations: 100000
- dirigeant . indépendant . revenu net de cotisations: 1000000
période:
- dirigeant . indépendant . revenu net de cotisations: 1000
période: mois
- dirigeant . indépendant . revenu net de cotisations: 5000
période: mois
inversions:
- entreprise . rémunération totale du dirigeant: 2000
- entreprise . rémunération totale du dirigeant: 50000
@ -43,4 +37,4 @@ impôt sur le revenu:
impôt . méthode de calcul: taux neutre
- dirigeant . indépendant . revenu net de cotisations: 20000
impôt . méthode de calcul: taux personnalisé
impôt . taux personnalisé: 0.1
impôt . taux personnalisé: 10

View File

@ -7,14 +7,6 @@
- entreprise . rémunération totale du dirigeant: 50000
- entreprise . rémunération totale du dirigeant: 100000
périodes:
- entreprise . rémunération totale du dirigeant: 200
période: mois
- entreprise . rémunération totale du dirigeant: 500
période: mois
- entreprise . rémunération totale du dirigeant: 5000
période: mois
avec charges:
- entreprise . rémunération totale du dirigeant: 10000
entreprise . charges: 2000

View File

@ -16,14 +16,6 @@
- contrat salarié . rémunération . brut de base: 100000
- contrat salarié . rémunération . brut de base: 1000000
périodes:
- contrat salarié . rémunération . brut de base: 3000
période: année
- contrat salarié . rémunération . brut de base: 45000
période: année
- contrat salarié . rémunération . brut de base: 500000
période: année
inversions:
- contrat salarié . prix du travail: 2000
- contrat salarié . rémunération . net: 2000
@ -57,7 +49,7 @@ cdd:
atmp:
- contrat salarié . rémunération . brut de base: 2000
contrat salarié . ATMP . taux collectif ATMP: 0.05
contrat salarié . ATMP . taux collectif ATMP: 5
assimilé salarié:
- dirigeant: assimilé salarié
@ -105,7 +97,7 @@ impôt sur le revenu:
établissement . localisation . département: Mayotte
- contrat salarié . rémunération . brut de base: 3000
impôt . méthode de calcul: taux personnalisé
impôt . taux personnalisé: 0.1
impôt . taux personnalisé: 10
heures supplémentaires:
- contrat salarié . rémunération . brut de base: 2000

View File

@ -2,7 +2,7 @@
// of simulations and persist their results in a snapshot (ie, a file commited in git). Our test runner,
// Jest, then compare the existing snapshot with the current Engine calculation and reports any difference.
//
// We only persist goals values in the file system, in order to be resilient to rule renaming (if a rule is
// We only persist targets values in the file system, in order to be resilient to rule renaming (if a rule is
// renamed the test configuration may be adapted but the persisted snapshot will remain unchanged).
/* eslint-disable no-undef */
@ -19,21 +19,25 @@ 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 Lib.Engine()
const runSimulations = (
situations,
goals,
targets,
baseSituation = {},
defaultUnits,
namePrefix = ''
) =>
Object.entries(situations).map(([name, situations]) =>
situations.forEach(situation => {
const res = Lib.evaluate(goals, { ...baseSituation, ...situation })
const res = engine.evaluate(targets, {
situation: { ...baseSituation, ...situation },
defaultUnits
})
// Stringify is not required, but allows the result to be displayed in a single
// line in the snapshot, which considerably reduce the number of lines of this snapshot
// and improve its readability.
expect(JSON.stringify(roundResult(res))).toMatchSnapshot(
namePrefix + name
namePrefix + ' ' + name
)
})
)
@ -42,23 +46,27 @@ it('calculate simulations-salarié', () => {
runSimulations(
employeeSituations,
employeeConfig.objectifs,
employeeConfig.situation
employeeConfig.situation,
['€/mois']
)
})
it('calculate simulations-indépendant', () => {
const goals = independantConfig.objectifs.reduce(
const targets = independantConfig.objectifs.reduce(
(acc, cur) => [...acc, ...cur.objectifs],
[]
)
runSimulations(independentSituations, goals, independantConfig.situation)
runSimulations(independentSituations, targets, independantConfig.situation, [
'€/an'
])
})
it('calculate simulations-auto-entrepreneur', () => {
runSimulations(
autoEntrepreneurSituations,
autoentrepreneurConfig.objectifs,
autoentrepreneurConfig.situation
autoentrepreneurConfig.situation,
['€/an']
)
})
@ -69,6 +77,7 @@ it('calculate simulations-rémunération-dirigeant', () => {
remunerationDirigeantSituations,
remunerationDirigeantConfig.objectifs,
{ ...baseSituation, ...situation },
['€/an'],
`${nom} - `
)
})
@ -78,6 +87,7 @@ it('calculate simulations-artiste-auteur', () => {
runSimulations(
artisteAuteurSituations,
artisteAuteurConfig.objectifs,
artisteAuteurConfig.situation
artisteAuteurConfig.situation,
['€/an']
)
})

View File

@ -46,20 +46,6 @@ describe('rule checks', function() {
)
expect(rulesNeedingDefault).to.be.empty
})
it('rules with a period should not have a flexible period', function() {
let problems = rules.filter(
({ defaultValue, période }) => période === 'flexible' && defaultValue
)
problems.map(({ dottedName }) =>
console.log(
'La valeur règle ',
dottedName,
" a une période flexible et une valeur par défaut. C'est un problème, car on ne sait pas pour quelle période ce défaut est défini. "
)
)
expect(problems).to.be.empty
})
})
it('rules with a formula should not have defaults', function() {

View File

@ -111,27 +111,6 @@ describe('analyse with mecanisms', function() {
).to.have.property('nodeValue', false)
})
it('should handle switch statements', function() {
let rawRules = [
{ nom: 'top' },
{
nom: 'top . startHere',
formule: {
'aiguillage numérique': {
'1 > dix': '1000%',
'3 < dix': '1100%',
'3 > dix': '1200%'
}
}
},
{ nom: 'top . dix', formule: 10 }
],
rules = parseAll(rawRules.map(enrichRule))
expect(
analyse(rules, 'startHere')(stateSelector).targets[0]
).to.have.property('nodeValue', 11)
})
it('should handle percentages', function() {
let rawRules = [{ nom: 'top' }, { nom: 'top . startHere', formule: '35%' }],
rules = parseAll(rawRules.map(enrichRule))
@ -353,7 +332,7 @@ describe('analyse with mecanisms', function() {
it('should handle filtering on components', function() {
let rawRules = [
{ nom: 'top' },
{ nom: 'top . startHere', formule: 'composed [salarié]' },
{ nom: 'top . startHere', formule: 'composed .salarié' },
{
nom: 'top . composed',
formule: {
@ -393,7 +372,7 @@ describe('analyse with mecanisms', function() {
{ nom: 'top' },
{
nom: 'top . startHere',
formule: 'composed [salarié] + composed [employeur]'
formule: 'composed .salarié + composed .employeur'
},
{ nom: 'top . orHere', formule: 'composed' },
{

View File

@ -1,5 +1,11 @@
import { expect } from 'chai'
import { removeOnce, parseUnit, inferUnit } from 'Engine/units'
import {
areUnitConvertible,
convertUnit,
inferUnit,
parseUnit,
removeOnce
} from 'Engine/units'
describe('Units', () => {
it('should remove the first element encounter in the list', () => {
@ -11,10 +17,26 @@ describe('Units', () => {
numerators: ['m'],
denominators: []
})
expect(parseUnit('/an')).to.deep.equal({
numerators: [],
denominators: ['an']
})
expect(parseUnit('m/s')).to.deep.equal({
numerators: ['m'],
denominators: ['s']
})
expect(parseUnit('kg.m/s')).to.deep.equal({
numerators: ['kg', 'm'],
denominators: ['s']
})
expect(parseUnit('kg.m/s')).to.deep.equal({
numerators: ['kg', 'm'],
denominators: ['s']
})
expect(parseUnit('€/personne/mois')).to.deep.equal({
numerators: ['€'],
denominators: ['personne', 'mois']
})
})
it('should work with simple use case *', () => {
let unit1 = { numerators: ['m'], denominators: ['s'] }
@ -47,3 +69,80 @@ describe('Units', () => {
})
})
})
describe('convertUnit', () => {
it('should convert month to year in denominator', () => {
expect(convertUnit(parseUnit('/mois'), parseUnit('/an'), 10)).to.eq(120)
})
it('should convert year to month in denominator', () => {
expect(convertUnit(parseUnit('/an'), parseUnit('/mois'), 120)).to.eq(10)
})
it('should convert year to month in numerator', () => {
expect(convertUnit(parseUnit('mois'), parseUnit('an'), 12)).to.eq(1)
})
it('should month to year in numerator', () => {
expect(convertUnit(parseUnit('mois'), parseUnit('an'), 12)).to.eq(1)
})
it('should convert percentage to simple value', () => {
expect(convertUnit(parseUnit('%'), parseUnit(''), 83)).to.closeTo(
0.83,
0.0000001
)
})
it('should convert more difficult value', () => {
expect(convertUnit(parseUnit('%/an'), parseUnit('/mois'), 12)).to.closeTo(
0.01,
0.0000001
)
})
it('should convert year, month, day, k€', () => {
expect(
convertUnit(
parseUnit('€/personne/jour'),
parseUnit('k€/an/personne'),
'100'
)
).to.closeTo(36.5, 0.0000001)
})
it('should handle simplification', () => {
expect(
convertUnit(parseUnit('€.an.%/mois'), parseUnit('€'), 100)
).to.closeTo(12, 0.0000001)
})
it('should handle complexification', () => {
expect(
convertUnit(parseUnit('€'), parseUnit('€.an.%/mois'), 12)
).to.closeTo(100, 0.0000001)
})
})
describe('areUnitConvertible', () => {
it('should be true for temporel unit', () => {
expect(areUnitConvertible(parseUnit('mois'), parseUnit('an'))).to.eq(true)
expect(areUnitConvertible(parseUnit('kg/an'), parseUnit('kg/mois'))).to.eq(
true
)
})
it('should be true for percentage', () => {
expect(areUnitConvertible(parseUnit('%/mois'), parseUnit('/an'))).to.eq(
true
)
})
it('should be true for more complicated cases', () => {
expect(
areUnitConvertible(
parseUnit('€/personne/mois'),
parseUnit('€/an/personne')
)
).to.eq(true)
})
it('should be false for unit not alike', () => {
expect(
areUnitConvertible(parseUnit('mois'), parseUnit('€/an/personne'))
).to.eq(false)
expect(areUnitConvertible(parseUnit('m.m'), parseUnit('m'))).to.eq(false)
expect(areUnitConvertible(parseUnit('m'), parseUnit('s'))).to.eq(false)
})
})
describe('simplifyUnit', () => {})