diff --git a/.vscode/settings.json b/.vscode/settings.json index 36cdf4ec8..6ebc57b1f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,9 +2,6 @@ "editor.formatOnSave": true, "spellright.language": ["fr", "en"], "spellright.documentTypes": ["yaml", "git-commit", "markdown"], - "editor.codeActionsOnSave": { - "source.organizeImports": true - }, "typescript.tsdk": "node_modules/typescript/lib", "editor.tabSize": 2 } diff --git a/circle.yml b/circle.yml index 778177bf2..bed49caf9 100644 --- a/circle.yml +++ b/circle.yml @@ -50,6 +50,7 @@ jobs: steps: - install - run: | + git config --global core.quotepath false yarn test yarn test-regressions diff --git a/componentTestSetup.js b/componentTestSetup.js index b328984b1..45fd41835 100644 --- a/componentTestSetup.js +++ b/componentTestSetup.js @@ -1,6 +1,6 @@ +import chai from 'chai' import Enzyme from 'enzyme' import Adapter from 'enzyme-adapter-react-16' -import chai from 'chai' import sinonChai from 'sinon-chai' chai.use(sinonChai) diff --git a/cypress/integration/mon-entreprise/covid19.js b/cypress/integration/mon-entreprise/covid19.js index 9826df5d4..478f21ee5 100644 --- a/cypress/integration/mon-entreprise/covid19.js +++ b/cypress/integration/mon-entreprise/covid19.js @@ -1,4 +1,13 @@ const fr = Cypress.env('language') === 'fr' +const testText = (selector, text) => + cy.get(`[data-test-id=${selector}]`).should($span => { + const displayedText = $span + .text() + .trim() + .replace(/[\s]/g, ' ') + console.log(displayedText, text) + expect(displayedText).to.eq(text) + }) describe('Page covid-19', function() { if (!fr) { @@ -11,12 +20,12 @@ describe('Page covid-19', function() { it('should display 100% de prise en charge pour un SMIC', () => { cy.get('input.currencyInput__input').click() cy.contains('SMIC').click() - cy.contains('Soit 100% du revenu net') - cy.contains('Soit 0% du coût habituel') + testText('comparaison-net', 'Soit 100 % du revenu net') + testText('comparaison-total', 'Soit 0 % du coût habituel') }) - it('should display 85% de prise en charge pour un salaire médian', () => { + it('should display 85 % de prise en charge pour un salaire médian', () => { cy.contains('salaire médian').click() - cy.contains('Soit 85% du revenu net') - cy.contains('Soit 0% du coût habituel') + testText('comparaison-net', 'Soit 85 % du revenu net') + testText('comparaison-total', 'Soit 0 % du coût habituel') }) }) diff --git a/cypress/integration/mon-entreprise/simulateurs.js b/cypress/integration/mon-entreprise/simulateurs.js index cb49daca4..81b51e821 100644 --- a/cypress/integration/mon-entreprise/simulateurs.js +++ b/cypress/integration/mon-entreprise/simulateurs.js @@ -20,7 +20,7 @@ describe('Simulateurs', function() { } cy.get(inputSelector).each((testedInput, i) => { cy.wrap(testedInput).type('{selectall}60000') - cy.wait(600) + cy.wait(800) cy.contains('Cotisations') cy.get(inputSelector).each(($input, j) => { const val = $input.val().replace(/[\s,.]/g, '') @@ -41,7 +41,7 @@ describe('Simulateurs', function() { if (['indépendant', 'assimilé-salarié'].includes(simulateur)) { cy.get(chargeInputSelector).type('{selectall}6000') } - cy.wait(600) + cy.wait(800) cy.contains('€ / mois').click() cy.get(inputSelector) .first() @@ -108,7 +108,7 @@ describe('Simulateurs', function() { cy.get(inputSelector) .first() .type('{selectall}5000') - cy.wait(600) + cy.wait(800) cy.get(inputSelector).each($input => { const val = +$input.val().replace(/[\s,.]/g, '') expect(val).not.to.be.below(4000) diff --git a/index.html b/index.html index 612f2fc07..0d0a62b0b 100644 --- a/index.html +++ b/index.html @@ -62,7 +62,7 @@ /> diff --git a/source/Provider.tsx b/source/Provider.tsx index 30cdab2b0..acbaf27ee 100644 --- a/source/Provider.tsx +++ b/source/Provider.tsx @@ -1,6 +1,8 @@ import { ThemeColorsProvider } from 'Components/utils/colors' -import { SitePathProvider, SitePaths } from 'Components/utils/withSitePaths' +import { EngineProvider } from 'Components/utils/EngineContext' +import { SitePathProvider, SitePaths } from 'Components/utils/SitePathsContext' import { TrackerProvider } from 'Components/utils/withTracker' +import Engine from 'Engine' import { createBrowserHistory } from 'history' import { AvailableLangs } from 'i18n' import i18next from 'i18next' @@ -12,6 +14,7 @@ import reducers, { RootState } from 'Reducers/rootReducer' import { applyMiddleware, compose, createStore, Middleware, Store } from 'redux' import thunk from 'redux-thunk' import Tracker from 'Tracker' +import { Rules } from './rules' import { inIframe } from './utils' declare global { diff --git a/source/actions/actions.ts b/source/actions/actions.ts index 939ff6ea1..041ea58e5 100644 --- a/source/actions/actions.ts +++ b/source/actions/actions.ts @@ -1,4 +1,4 @@ -import { SitePaths } from 'Components/utils/withSitePaths' +import { SitePaths } from 'Components/utils/SitePathsContext' import { History } from 'history' import { RootState, SimulationConfig } from 'Reducers/rootReducer' import { ThunkAction } from 'redux-thunk' @@ -12,13 +12,12 @@ export type Action = | UpdateAction | SetSimulationConfigAction | DeletePreviousSimulationAction - | SetExempleAction | ExplainVariableAction | UpdateSituationAction | HideControlAction | LoadPreviousSimulationAction | SetSituationBranchAction - | UpdateDefaultUnitAction + | UpdateTargetUnitAction | SetActiveTargetAction | CompanyStatusAction @@ -46,18 +45,6 @@ type DeletePreviousSimulationAction = { type: 'DELETE_PREVIOUS_SIMULATION' } -type SetExempleAction = - | { - type: 'SET_EXAMPLE' - name: null - } - | { - type: 'SET_EXAMPLE' - name: string - situation: object - dottedName: DottedName - } - type ResetSimulationAction = ReturnType type UpdateAction = ReturnType type UpdateSituationAction = ReturnType @@ -66,14 +53,14 @@ type SetSituationBranchAction = ReturnType type SetActiveTargetAction = ReturnType type HideControlAction = ReturnType type ExplainVariableAction = ReturnType -type UpdateDefaultUnitAction = ReturnType +type UpdateTargetUnitAction = ReturnType export const resetSimulation = () => ({ type: 'RESET_SIMULATION' } as const) -export const goToQuestion = (question: string) => +export const goToQuestion = (question: DottedName) => ({ type: 'STEP_ACTION', name: 'unfold', @@ -99,7 +86,7 @@ export const setSituationBranch = (id: number) => } as const) export const setSimulationConfig = ( - config: Object, + config: SimulationConfig, useCompanyDetails: boolean = false ): ThunkResult => (dispatch, getState, { history }): void => { if (getState().simulation?.config === config) { @@ -134,26 +121,17 @@ export const updateSituation = (fieldName: DottedName, value: unknown) => value } as const) -export const updateUnit = (defaultUnit: string) => +export const updateUnit = (targetUnit: string) => ({ - type: 'UPDATE_DEFAULT_UNIT', - defaultUnit + type: 'UPDATE_TARGET_UNIT', + targetUnit } as const) -export function setExample( - name: string, - situation: Situation, - dottedName: DottedName -) { - return { type: 'SET_EXAMPLE', name, situation, dottedName } as const -} - export const goBackToSimulation = (): ThunkResult => ( - dispatch, + _, getState, { history } ) => { - dispatch({ type: 'SET_EXAMPLE', name: null }) const url = getState().simulation?.url url && history.push(url) } diff --git a/source/components/Banner.tsx b/source/components/Banner.tsx index d751ae5d1..881e549ad 100644 --- a/source/components/Banner.tsx +++ b/source/components/Banner.tsx @@ -1,7 +1,7 @@ import React from 'react' import emoji from 'react-easy-emoji' import { useSelector } from 'react-redux' -import { firstStepCompletedSelector } from 'Selectors/analyseSelectors' +import { firstStepCompletedSelector } from 'Selectors/simulationSelectors' import Animate from 'Ui/animate' import './Banner.css' diff --git a/source/components/BarChart.tsx b/source/components/BarChart.tsx index 466a01cf0..d85309a4b 100644 --- a/source/components/BarChart.tsx +++ b/source/components/BarChart.tsx @@ -4,31 +4,34 @@ import emoji from 'react-easy-emoji' import { animated, config, useSpring } from 'react-spring' import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting' import { ThemeColorsContext } from 'Components/utils/colors' +import { formatValue } from 'Engine/format' +import { useTranslation } from 'react-i18next' const ANIMATION_SPRING = config.gentle -let ChartItemBar = ({ styles, color, numberToPlot, unit }) => ( -
- -
- - {numberToPlot} - +let ChartItemBar = ({ styles, color, numberToPlot, unit }) => { + const language = useTranslation().i18n.language + return ( +
+ +
+ {formatValue({ nodeValue: numberToPlot, unit, precision: 0, language })} +
-
-) + ) +} let BranchIcône = ({ icône }) => (
{emoji(icône)} diff --git a/source/components/Controls.tsx b/source/components/Controls.tsx index a55a2da3c..087c2580a 100644 --- a/source/components/Controls.tsx +++ b/source/components/Controls.tsx @@ -1,24 +1,22 @@ import { goToQuestion, hideControl } from 'Actions/actions' +import { useControls, useInversionFail } from 'Components/utils/EngineContext' import { makeJsx } from 'Engine/evaluation' import React from 'react' import emoji from 'react-easy-emoji' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' -import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' import animate from 'Ui/animate' import './Controls.css' import { Markdown } from './utils/markdown' import { ScrollToElement } from './utils/Scroll' +import { answeredQuestionsSelector } from 'Selectors/simulationSelectors' export default function Controls() { const { t } = useTranslation() - const foldedSteps = useSelector( - (state: RootState) => state.simulation?.foldedSteps - ) - const analysis = useSelector(analysisWithDefaultsSelector) - const controls = analysis?.controls - const inversionFail = analysis?.cache._meta.inversionFail + const answeredQuestions = useSelector(answeredQuestionsSelector) + const controls = useControls() + const inversionFail = useInversionFail() const hiddenControls = useSelector( (state: RootState) => state.simulation?.hiddenControls ) @@ -56,7 +54,7 @@ export default function Controls() { {makeJsx(evaluated)} )} - {solution && !foldedSteps?.includes(solution.cible) && ( + {solution && !answeredQuestions?.includes(solution.cible) && (
+ )} + + ) +} + +let Example = ({ + ex: { nom, situation }, + rule, + currentExample, + setCurrentExample +}) => { + let selected = currentExample && currentExample.name == nom + return ( +
  • + +
  • + ) +} diff --git a/source/components/rule/Header.css b/source/components/Documentation/Header.css similarity index 100% rename from source/components/rule/Header.css rename to source/components/Documentation/Header.css diff --git a/source/components/rule/Header.js b/source/components/Documentation/Header.js similarity index 100% rename from source/components/rule/Header.js rename to source/components/Documentation/Header.js diff --git a/source/components/rule/Namespace.css b/source/components/Documentation/Namespace.css similarity index 100% rename from source/components/rule/Namespace.css rename to source/components/Documentation/Namespace.css diff --git a/source/components/rule/Namespace.tsx b/source/components/Documentation/Namespace.tsx similarity index 86% rename from source/components/rule/Namespace.tsx rename to source/components/Documentation/Namespace.tsx index ff5378342..34dac6750 100644 --- a/source/components/rule/Namespace.tsx +++ b/source/components/Documentation/Namespace.tsx @@ -1,18 +1,19 @@ import { ThemeColorsContext } from 'Components/utils/colors' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' import { useSelector } from 'react-redux' import { Link } from 'react-router-dom' import { DottedName } from 'Rules' -import { parsedRulesSelector } from 'Selectors/analyseSelectors' import { capitalise0 } from '../../utils' import './Namespace.css' +import { EngineContext } from 'Components/utils/EngineContext' export default function Namespace({ dottedName }: { dottedName: DottedName }) { const sitePaths = useContext(SitePathsContext) const colors = useContext(ThemeColorsContext) - const flatRules = useSelector(parsedRulesSelector) + const rules = useContext(EngineContext).getParsedRules() + return (
      {dottedName @@ -27,7 +28,7 @@ export default function Namespace({ dottedName }: { dottedName: DottedName }) { ) .map((fragments: string[]) => { let ruleName = fragments.join(' . ') as DottedName, - rule = flatRules[ruleName] + rule = rules[ruleName] if (!rule) { throw new Error( `Attention, il se peut que la règle ${ruleName}, ait été définie avec un namespace qui n'existe pas.` diff --git a/source/components/rule/References.css b/source/components/Documentation/References.css similarity index 72% rename from source/components/rule/References.css rename to source/components/Documentation/References.css index 861291afb..511940ab2 100644 --- a/source/components/rule/References.css +++ b/source/components/Documentation/References.css @@ -1,12 +1,9 @@ .references { - font-size: 100%; list-style: none; - padding-left: 1em; - color: #333350; + padding: 0; } .references a { - color: inherit; - width: 40%; + flex: 1; text-decoration: underline; } @@ -20,15 +17,6 @@ text-align: left; font-style: italic; } -.references .url { - font-weight: 600; - color: white; - background: #333350; - border-radius: 0.4em; - font-style: italic; - padding: 0.05em 0.6em; - font-size: 95%; -} .references .imageWrapper { width: 6rem; height: 3rem; diff --git a/source/components/rule/References.tsx b/source/components/Documentation/References.tsx similarity index 83% rename from source/components/rule/References.tsx rename to source/components/Documentation/References.tsx index d8f18a8e6..71141fb55 100644 --- a/source/components/rule/References.tsx +++ b/source/components/Documentation/References.tsx @@ -23,16 +23,28 @@ function Ref({ name, link }: RefProps) { refData = (refKey && references[refKey]) || {}, domain = cleanDomain(link) return ( -
    • +
    • {refData.image && ( )} - + {capitalise0(name)} - {domain} + {domain}
    • ) } diff --git a/source/components/Documentation/Rule.js b/source/components/Documentation/Rule.js new file mode 100644 index 000000000..38ba99d14 --- /dev/null +++ b/source/components/Documentation/Rule.js @@ -0,0 +1,199 @@ +import { ThemeColorsContext } from 'Components/utils/colors' +import { EngineContext, useEvaluation } from 'Components/utils/EngineContext' +import { SitePathsContext } from 'Components/utils/SitePathsContext' +import { formatValue } from 'Engine/format' +import mecanisms from 'Engine/mecanisms.yaml' +import { serializeUnit } from 'Engine/units' +import { filter, isEmpty } from 'ramda' +import React, { Suspense, useContext, useState } from 'react' +import emoji from 'react-easy-emoji' +import { Helmet } from 'react-helmet' +import { Trans, useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import Animate from 'Ui/animate' +import { AttachDictionary } from '../AttachDictionary' +import RuleLink from '../RuleLink' +import { Markdown } from '../utils/markdown' +import Algorithm from './Algorithm' +import Examples from './Examples' +import RuleHeader from './Header' +import References from './References' +import { UseDefaultValuesContext } from './UseDefaultValuesContext' + +let LazySource = React.lazy(() => import('./RuleSource')) + +export default AttachDictionary(mecanisms)(function Rule({ dottedName }) { + const [currentExample, setCurrentExample] = useState(null) + const rules = useContext(EngineContext).getParsedRules() + const useDefaultValues = useContext(UseDefaultValuesContext) + const rule = useEvaluation(dottedName, { useDefaultValues }) + const [viewSource, setViewSource] = useState(false) + const { t, i18n } = useTranslation() + let { type, name, acronyme, title, description, question, icon } = rule, + namespaceRules = filter( + rule => + rule.dottedName.startsWith(dottedName) && + rule.dottedName.split(' . ').length === + dottedName.split(' . ').length + 1, + rules + ) + + const renderReferences = ({ références: refs }) => + refs ? ( +
      +

      + Références +

      + +
      + ) : null + + return ( +
      + + + + {(rule.nodeValue || rule.defaultValue || rule.unit) && ( + <> +

      + {rule.nodeValue != null && ( + <> + {formatValue({ ...rule, language: i18n.language })} +
      + + )} + {rule.defaultValue?.nodeValue != null && ( + <> + + Valeur par défaut :{' '} + {formatValue({ + ...rule.defaultValue, + language: i18n.language + })} + +
      + + )} + {rule.nodeValue == null && !rule.defaultValue?.unit && rule.unit && ( + <> + Unité : {serializeUnit(rule.unit)} + + )} +

      + + )} + + + + {viewSource === dottedName ? ( + Chargement du code source...
      }> + + + ) : ( +
      + +
      + )} + + {rule['rend non applicable'] && ( + <> +

      + Rend non applicable les règles suivantes :{' '} +

      +
        + {rule['rend non applicable'].map(ruleName => ( +
      • + +
      • + ))} +
      + + )} + {rule.note && ( + <> +

      Note

      +
      + +
      + + )} + {renderReferences(rule)} + + + {!isEmpty(namespaceRules) && ( + + )} + +
    + ) +}) + +function NamespaceRulesList({ namespaceRules }) { + const colors = useContext(ThemeColorsContext) + const sitePaths = useContext(SitePathsContext) + const useDefaultValues = useContext(UseDefaultValuesContext) + return ( +
    +

    + Pages associées +

    +
      + {Object.values(namespaceRules).map(r => ( +
    • + + {r.title || r.name} + +
    • + ))} +
    +
    + ) +} diff --git a/source/components/rule/RuleSource.tsx b/source/components/Documentation/RuleSource.tsx similarity index 54% rename from source/components/rule/RuleSource.tsx rename to source/components/Documentation/RuleSource.tsx index a038f3b81..70d36d17c 100644 --- a/source/components/rule/RuleSource.tsx +++ b/source/components/Documentation/RuleSource.tsx @@ -1,7 +1,6 @@ import { ParsedRule } from 'Engine/types' import { safeDump } from 'js-yaml' import React from 'react' -import emoji from 'react-easy-emoji' import rules from 'Rules' import PublicodeHighlighter from '../ui/PublicodeHighlighter' @@ -11,13 +10,15 @@ export default function RuleSource({ dottedName }: RuleSourceProps) { let source = rules[dottedName] return ( -
    -

    - {emoji('⚙️ ')} - Code source
    - {dottedName} -

    +
    +

    Source publicode

    -
    +

    + Ci-dessus la règle d'origine, écrite en publicode. Publicode est un + langage déclaratif développé par beta.gouv.fr en partenariat avec + l'Acoss pour encoder les algorithmes d'intérêt public.{' '} + En savoir plus. +

    + ) } diff --git a/source/sites/mon-entreprise.fr/pages/Documentation/RulesList.css b/source/components/Documentation/RulesList.css similarity index 100% rename from source/sites/mon-entreprise.fr/pages/Documentation/RulesList.css rename to source/components/Documentation/RulesList.css diff --git a/source/sites/mon-entreprise.fr/pages/Documentation/RulesList.tsx b/source/components/Documentation/RulesList.tsx similarity index 70% rename from source/sites/mon-entreprise.fr/pages/Documentation/RulesList.tsx rename to source/components/Documentation/RulesList.tsx index 4368b3a84..1000aa410 100644 --- a/source/sites/mon-entreprise.fr/pages/Documentation/RulesList.tsx +++ b/source/components/Documentation/RulesList.tsx @@ -1,12 +1,12 @@ import SearchBar from 'Components/SearchBar' -import React from 'react' +import React, { useContext } from 'react' import { Trans } from 'react-i18next' import { useSelector } from 'react-redux' -import { parsedRulesSelector } from 'Selectors/analyseSelectors' import './RulesList.css' +import { EngineContext } from 'Components/utils/EngineContext' export default function RulesList() { - const rules = useSelector(parsedRulesSelector) + const rules = useContext(EngineContext).getParsedRules() return (

    diff --git a/source/components/Documentation/UseDefaultValuesContext.tsx b/source/components/Documentation/UseDefaultValuesContext.tsx new file mode 100644 index 000000000..92ae87256 --- /dev/null +++ b/source/components/Documentation/UseDefaultValuesContext.tsx @@ -0,0 +1,3 @@ +import { createContext } from 'react' + +export const UseDefaultValuesContext = createContext(true) diff --git a/source/components/Documentation/index.tsx b/source/components/Documentation/index.tsx new file mode 100644 index 000000000..d2e467fc2 --- /dev/null +++ b/source/components/Documentation/index.tsx @@ -0,0 +1,31 @@ +import RulePage from 'Components/RulePage' +import { EngineProvider } from 'Components/utils/EngineContext' +import Engine from 'Engine' +import React from 'react' +import { Route, Switch } from 'react-router' +import { DottedName } from 'Rules' +import RulesList from './RulesList' +import { UseDefaultValuesContext } from './UseDefaultValuesContext' + +type DocumentationProps = { + basePath: string + engine: Engine + useDefaultValues?: boolean +} + +export default function Documentation({ + basePath, + engine, + useDefaultValues = false +}: DocumentationProps) { + return ( + + + + + + + + + ) +} diff --git a/source/components/EngineValue.tsx b/source/components/EngineValue.tsx new file mode 100644 index 000000000..0cbb3a196 --- /dev/null +++ b/source/components/EngineValue.tsx @@ -0,0 +1,65 @@ +import Engine from 'Engine' +import { formatValue } from 'Engine/format' +import { EvaluatedNode } from 'Engine/types' +import React, { useContext } from 'react' +import { useTranslation } from 'react-i18next' +import { DottedName } from 'Rules' +import { coerceArray } from '../utils' +import RuleLink from './RuleLink' +import { EngineContext } from './utils/EngineContext' + +export type ValueProps = { + expression: string + unit?: string + displayedUnit?: string + precision?: number + engine?: Engine + linkToRule?: boolean +} & React.HTMLProps + +export default function Value({ + expression, + unit, + displayedUnit, + precision, + engine, + linkToRule = true, + ...props +}: ValueProps) { + const { language } = useTranslation().i18n + if (expression === null) { + throw new TypeError('expression cannot be null') + } + const evaluation = (engine ?? useContext(EngineContext)).evaluate( + expression, + { unit } + ) + const nodeValue = evaluation.nodeValue + const value = formatValue({ + nodeValue, + unit: + displayedUnit ?? (evaluation as EvaluatedNode).unit, + language, + precision + }) + if ('dottedName' in evaluation && linkToRule) { + return ( + + {value} + + ) + } + return {value} +} + +type ConditionProps = { + expression: string | string[] + children: React.ReactNode +} +export function Condition({ expression, children }: ConditionProps) { + const engine = useContext(EngineContext) + if (!coerceArray(expression).every(expr => engine.evaluate(expr).nodeValue)) { + return null + } + return <>{children} +} diff --git a/source/components/Mecanisms.css b/source/components/Mecanisms.css deleted file mode 100644 index 734d125f9..000000000 --- a/source/components/Mecanisms.css +++ /dev/null @@ -1,7 +0,0 @@ -ul#mecanisms { - margin: 3em auto; -} - -#mecanisms .warning { - color: #e74c3c; -} diff --git a/source/components/MoreInfosOnUs.tsx b/source/components/MoreInfosOnUs.tsx index 3dbcee0d9..c7f241ae2 100644 --- a/source/components/MoreInfosOnUs.tsx +++ b/source/components/MoreInfosOnUs.tsx @@ -3,7 +3,7 @@ import emoji from 'react-easy-emoji' import { useTranslation } from 'react-i18next' import { Link, useLocation } from 'react-router-dom' import { icons } from './ui/SocialIcon' -import { SitePathsContext } from './utils/withSitePaths' +import { SitePathsContext } from './utils/SitePathsContext' export default function MoreInfosOnUs() { const { pathname } = useLocation() diff --git a/source/components/PaySlip.css b/source/components/PaySlip.css index 2e2bef903..c8d370dfe 100644 --- a/source/components/PaySlip.css +++ b/source/components/PaySlip.css @@ -31,6 +31,9 @@ background-color: transparent !important; } +.payslip__container h5 { + margin: 0; +} .payslip__container h5 { margin: 0; } @@ -41,6 +44,13 @@ text-transform: capitalize; } +.payslip__container span { + display: flex; + align-items: flex-end; + font-family: 'Courier New', Courier, monospace; + justify-content: flex-end; +} + .payslip__cotisationsSection h4:not(:first-child) { text-align: right; } diff --git a/source/components/PaySlip.tsx b/source/components/PaySlip.tsx index f944e8079..de786014c 100644 --- a/source/components/PaySlip.tsx +++ b/source/components/PaySlip.tsx @@ -1,28 +1,66 @@ -import { ThemeColorsContext } from 'Components/utils/colors' -import Value from 'Components/Value' -import { getRuleFromAnalysis } from 'Engine/ruleUtils' +import { useEvaluation, EngineContext } from 'Components/utils/EngineContext' +import Value from 'Components/EngineValue' +import { formatValue } from 'Engine/format' import React, { Fragment, useContext } from 'react' import { Trans } from 'react-i18next' -import { useSelector } from 'react-redux' -import { - analysisWithDefaultsSelector, - parsedRulesSelector -} from 'Selectors/analyseSelectors' -import { analysisToCotisationsSelector } from 'Selectors/ficheDePaieSelectors' +import { DottedName } from 'Rules' import './PaySlip.css' import { Line, SalaireBrutSection, SalaireNetSection } from './PaySlipSections' import RuleLink from './RuleLink' -export default function PaySlip() { - const { lightestColor } = useContext(ThemeColorsContext) - const cotisations = useSelector(analysisToCotisationsSelector) - const analysis = useSelector(analysisWithDefaultsSelector) - const parsedRules = useSelector(parsedRulesSelector) - let getRule = getRuleFromAnalysis(analysis) +export const SECTION_ORDER = [ + 'protection sociale . santé', + 'protection sociale . accidents du travail et maladies professionnelles', + 'protection sociale . retraite', + 'protection sociale . famille', + 'protection sociale . assurance chômage', + 'protection sociale . formation', + 'protection sociale . transport', + 'protection sociale . autres' +] as const - const heuresSupplémentaires = getRule( - 'contrat salarié . temps de travail . heures supplémentaires' - ) +type Section = typeof SECTION_ORDER[number] + +function getSection(rule): Section { + const section = ('protection sociale . ' + + (rule.cotisation?.branche ?? rule.taxe?.branche)) as Section + if (SECTION_ORDER.includes(section)) { + return section + } + return 'protection sociale . autres' +} + +export function getCotisationsBySection( + parsedRules +): Array<[Section, DottedName[]]> { + const cotisations = [ + ...parsedRules['contrat salarié . cotisations . patronales'].formule + .explanation.explanation, + ...parsedRules['contrat salarié . cotisations . salariales'].formule + .explanation.explanation + ] + .map(cotisation => cotisation.dottedName) + .filter(Boolean) + .reduce((acc, cotisation) => { + const sectionName = getSection(parsedRules[cotisation]) + return { + ...acc, + [sectionName]: (acc[sectionName] ?? new Set()).add(cotisation) + } + }, {}) as Record> + + return Object.entries(cotisations) + .map(([section, dottedNames]) => [section, [...dottedNames.values()]]) + .sort( + ([a], [b]) => + SECTION_ORDER.indexOf(a as Section) - + SECTION_ORDER.indexOf(b as Section) + ) as Array<[Section, DottedName[]]> +} + +export default function PaySlip() { + const parsedRules = useContext(EngineContext).getParsedRules() + const cotisationsBySection = getCotisationsBySection(parsedRules) return (
    + - {!!heuresSupplémentaires?.nodeValue && ( - - )}
    - + {/* Section cotisations */}

    @@ -63,34 +99,15 @@ export default function PaySlip() {

    Part salarié

    - {cotisations.map(([brancheDottedName, cotisationList]) => { - let branche = parsedRules[brancheDottedName] + {cotisationsBySection.map(([sectionDottedName, cotisations]) => { + let section = parsedRules[sectionDottedName] return ( - +
    - +
    - {cotisationList.map(cotisation => ( - - - - {cotisation.montant.partPatronale} - - - {cotisation.montant.partSalariale} - - + {cotisations.map(cotisation => ( + ))}
    ) @@ -101,23 +118,57 @@ export default function PaySlip() { Total des retenues
    {/* Salaire chargé */} - +
    {/* Section salaire net */} - +

    ) } + +function Cotisation({ dottedName }: { dottedName: DottedName }) { + const parsedRules = useContext(EngineContext).getParsedRules() + + const partSalariale = useEvaluation( + 'contrat salarié . cotisations . salariales' + )?.formule.explanation.explanation.find( + cotisation => cotisation.dottedName === dottedName + ) + const partPatronale = useEvaluation( + 'contrat salarié . cotisations . patronales' + )?.formule.explanation.explanation.find( + cotisation => cotisation.dottedName === dottedName + ) + if (!partPatronale?.nodeValue && !partSalariale?.nodeValue) { + return null + } + return ( + <> + + + {partPatronale?.nodeValue + ? formatValue({ ...partPatronale, unit: '€' }) + : '–'} + + + {partSalariale?.nodeValue + ? formatValue({ ...partSalariale, unit: '€' }) + : '–'} + + + ) +} diff --git a/source/components/PaySlipSections.tsx b/source/components/PaySlipSections.tsx index cc5b577b3..cd8123fcb 100644 --- a/source/components/PaySlipSections.tsx +++ b/source/components/PaySlipSections.tsx @@ -1,125 +1,94 @@ -import Value from 'Components/Value' -import { EvaluatedRule } from 'Engine/types' -import React from 'react' +import { EngineContext } from 'Components/utils/EngineContext' +import Value, { ValueProps, Condition } from 'Components/EngineValue' +import React, { useContext } from 'react' import { Trans } from 'react-i18next' -import { useSelector } from 'react-redux' import { DottedName } from 'Rules' -import { defaultUnitSelector } from 'Selectors/analyseSelectors' +import { coerceArray } from '../utils' import RuleLink from './RuleLink' -export let SalaireBrutSection = ({ - getRule -}: { - getRule: (rule: DottedName) => EvaluatedRule -}) => { - let avantagesEnNature = getRule( - 'contrat salarié . rémunération . avantages en nature' - ), - indemnitésSalarié = getRule('contrat salarié . CDD . indemnités salarié'), - remboursementDeFrais = getRule('contrat salarié . frais professionnels'), - heuresSupplémentaires = getRule( - 'contrat salarié . rémunération . heures supplémentaires' - ), - salaireDeBase = getRule('contrat salarié . rémunération . brut de base'), - rémunérationBrute = getRule('contrat salarié . rémunération . brut'), - chômagePartielIndemnité = getRule( - 'contrat salarié . activité partielle . indemnités' - ), - chômagePartielAbsence = getRule( - 'contrat salarié . activité partielle . retrait absence' - ), - primes = getRule('contrat salarié . rémunération . primes') +export let SalaireBrutSection = () => { return (

    Salaire

    - - {!!avantagesEnNature?.nodeValue && ( - - )} - {chômagePartielIndemnité?.nodeValue && ( - <> - - - - )} - {!!heuresSupplémentaires?.nodeValue && ( - - )} - {!!primes?.nodeValue && } - {!!remboursementDeFrais?.nodeValue && ( - - )} - {!!indemnitésSalarié?.nodeValue && } - {rémunérationBrute.nodeValue !== salaireDeBase.nodeValue && ( - - )} + + + + + + + + + + + +
    ) } -export let Line = ({ rule, className = '', ...props }) => { - const defaultUnit = useSelector(defaultUnitSelector) - return ( - <> - - - - ) -} - -export let SalaireNetSection = ({ getRule }) => { - let avantagesEnNature = getRule( - 'contrat salarié . rémunération . avantages en nature . montant' - ) - let impôt = getRule('impôt') - let netImposable = getRule('contrat salarié . rémunération . net imposable') - const retenueTitresRestaurant = getRule( - 'contrat salarié . frais professionnels . titres-restaurant . montant' - ) +export let SalaireNetSection = () => { return (

    Salaire net

    - {netImposable && } - {(avantagesEnNature?.nodeValue || retenueTitresRestaurant?.nodeValue) && ( - - )} - {!!avantagesEnNature?.nodeValue && ( - - )} - {!!retenueTitresRestaurant?.nodeValue && ( - - )} - + + + + + + - {!!impôt && ( - <> - - - - )} + + + +
    ) } + +type LineProps = { + rule: DottedName + negative?: boolean +} & Omit + +export function Line({ + rule, + displayedUnit = '€', + negative = false, + className, + ...props +}: LineProps) { + const parsedRules = useContext(EngineContext).getParsedRules() + return ( + + + + + ) +} diff --git a/source/components/PercentageField.tsx b/source/components/PercentageField.tsx index 36cb92eac..0333bcb5a 100644 --- a/source/components/PercentageField.tsx +++ b/source/components/PercentageField.tsx @@ -1,5 +1,6 @@ import { formatValue } from 'Engine/format' import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' import { debounce as debounceFn } from '../utils' import './PercentageField.css' @@ -9,6 +10,7 @@ export default function PercentageField({ onChange, value, debounce = 0 }) { debounce ? debounceFn(debounce, onChange) : onChange, [debounce, onChange] ) + const language = useTranslation().i18n.language return (
    @@ -28,7 +30,8 @@ export default function PercentageField({ onChange, value, debounce = 0 }) { /> {formatValue({ - value: localValue, + nodeValue: localValue, + language, unit: '%' })} diff --git a/source/components/PeriodSwitch.tsx b/source/components/PeriodSwitch.tsx index a42b7be43..73bc010bd 100644 --- a/source/components/PeriodSwitch.tsx +++ b/source/components/PeriodSwitch.tsx @@ -3,13 +3,13 @@ import { parseUnit, serializeUnit } from 'Engine/units' import React from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { defaultUnitSelector } from 'Selectors/analyseSelectors' +import { targetUnitSelector } from 'Selectors/simulationSelectors' import './PeriodSwitch.css' export default function PeriodSwitch() { const dispatch = useDispatch() const language = useTranslation().i18n.language - const currentUnit = useSelector(defaultUnitSelector) + const currentUnit = useSelector(targetUnitSelector) let units = ['€/mois', '€/an'] return ( diff --git a/source/components/PreviousSimulationBanner.tsx b/source/components/PreviousSimulationBanner.tsx index 93445c06e..7544b3fc2 100644 --- a/source/components/PreviousSimulationBanner.tsx +++ b/source/components/PreviousSimulationBanner.tsx @@ -3,15 +3,15 @@ import React from 'react' import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' -import { noUserInputSelector } from 'Selectors/analyseSelectors' import { LinkButton } from 'Ui/Button' import Banner from './Banner' +import { firstStepCompletedSelector } from 'Selectors/simulationSelectors' export default function PreviousSimulationBanner() { const previousSimulation = useSelector( (state: RootState) => state.previousSimulation ) - const newSimulationStarted = !useSelector(noUserInputSelector) + const newSimulationStarted = useSelector(firstStepCompletedSelector) const dispatch = useDispatch() return ( diff --git a/source/components/QuickLinks.tsx b/source/components/QuickLinks.tsx index 5a22aa4f8..246863f7a 100644 --- a/source/components/QuickLinks.tsx +++ b/source/components/QuickLinks.tsx @@ -5,20 +5,20 @@ import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' import { DottedName } from 'Rules' + +import { useNextQuestions } from './utils/useNextQuestion' import { - currentQuestionSelector, - nextStepsSelector -} from 'Selectors/analyseSelectors' + answeredQuestionsSelector, + currentQuestionSelector +} from 'Selectors/simulationSelectors' export default function QuickLinks() { const currentQuestion = useSelector(currentQuestionSelector) - const nextSteps = useSelector(nextStepsSelector) + const nextSteps = useNextQuestions() const quickLinks = useSelector( (state: RootState) => state.simulation?.config.questions?.["à l'affiche"] ) - const quickLinksToHide = useSelector( - (state: RootState) => state.simulation?.foldedSteps || [] - ) + const quickLinksToHide = useSelector(answeredQuestionsSelector) const dispatch = useDispatch() if (!quickLinks) { diff --git a/source/components/RuleLink.tsx b/source/components/RuleLink.tsx index 0493b100d..e6738a13b 100644 --- a/source/components/RuleLink.tsx +++ b/source/components/RuleLink.tsx @@ -1,5 +1,5 @@ import { ThemeColorsContext } from 'Components/utils/colors' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import { nameLeaf } from 'Engine/ruleUtils' import { ParsedRule } from 'Engine/types' import React, { useContext } from 'react' @@ -25,10 +25,14 @@ export default function RuleLink({ const sitePaths = useContext(SitePathsContext) const { color } = useContext(ThemeColorsContext) const newPath = sitePaths.documentation.rule(dottedName) - return ( { return (
    - +
    {valuesToShow ? : } - {brancheName && {brancheName}}
    diff --git a/source/components/SalaryExplanation.tsx b/source/components/SalaryExplanation.tsx index 94ce70af2..99f04887e 100644 --- a/source/components/SalaryExplanation.tsx +++ b/source/components/SalaryExplanation.tsx @@ -2,29 +2,20 @@ import Distribution from 'Components/Distribution' import PaySlip from 'Components/PaySlip' import StackedBarChart from 'Components/StackedBarChart' import { ThemeColorsContext } from 'Components/utils/colors' -import { getRuleFromAnalysis } from 'Engine/ruleUtils' +import { useEvaluation, useInversionFail } from 'Components/utils/EngineContext' import React, { useContext, useRef } from 'react' import emoji from 'react-easy-emoji' import { Trans, useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' -import { - analysisWithDefaultsSelector, - defaultUnitSelector -} from 'Selectors/analyseSelectors' import * as Animate from 'Ui/animate' +import { answeredQuestionsSelector } from 'Selectors/simulationSelectors' export default function SalaryExplanation() { - const showDistributionFirst = useSelector( - (state: RootState) => !state.simulation?.foldedSteps.length - ) - const analysis = useSelector(analysisWithDefaultsSelector) - const inversionFail = analysis?.cache._meta.inversionFail + const showDistributionFirst = !useSelector(answeredQuestionsSelector).length const distributionRef = useRef(null) - // We can't provide an explanation if the engine has failed to run the - // simulation. - if (inversionFail) { + if (useInversionFail()) { return null } return ( @@ -83,11 +74,16 @@ export default function SalaryExplanation() { } function RevenueRepatitionSection() { - const analysis = useSelector(analysisWithDefaultsSelector) - const getRule = getRuleFromAnalysis(analysis) const { t } = useTranslation() const { palettes } = useContext(ThemeColorsContext) - + const data = useEvaluation( + [ + 'contrat salarié . rémunération . net après impôt', + 'impôt', + 'contrat salarié . cotisations' + ], + { unit: '€/mois' } + ) return (

    @@ -96,18 +92,17 @@ function RevenueRepatitionSection() {

    - {unit?.endsWith('mois') ? ( - Fiche de paie - ) : ( - Détail annuel des cotisations - )} + Fiche de paie

    diff --git a/source/components/SchemeComparaison.tsx b/source/components/SchemeComparaison.tsx index de60c8dd7..d5fff8de8 100644 --- a/source/components/SchemeComparaison.tsx +++ b/source/components/SchemeComparaison.tsx @@ -1,4 +1,4 @@ -import { setSimulationConfig, setSituationBranch } from 'Actions/actions' +import { setSimulationConfig } from 'Actions/actions' import { defineDirectorStatus, isAutoentrepreneur @@ -6,34 +6,24 @@ import { import classnames from 'classnames' 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 { SitePathsContext } from 'Components/utils/withSitePaths' -import Value from 'Components/Value' -import { getRuleFromAnalysis } from 'Engine/ruleUtils' +import Value from 'Components/EngineValue' +import dirigeantComparaison from 'Components/simulationConfigs/rémunération-dirigeant.yaml' +import Engine from 'Engine' import revenusSVG from 'Images/revenus.svg' -import { default as React, useCallback, useContext, useState } from 'react' +import { + default as React, + useCallback, + useContext, + useMemo, + useState +} from 'react' import emoji from 'react-easy-emoji' import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { Link } from 'react-router-dom' -import { RootState } from 'Reducers/rootReducer' -import { DottedName } from 'Rules' -import { - analysisWithDefaultsSelector, - branchAnalyseSelector -} from 'Selectors/analyseSelectors' -import Animate from 'Ui/animate' +import { situationSelector } from 'Selectors/simulationSelectors' import InfoBulle from 'Ui/InfoBulle' import './SchemeComparaison.css' - -let getBranchIndex = (branch: string) => - ({ assimilé: 0, indépendant: 1, 'auto-entrepreneur': 2 }[branch]) - -let getRuleFrom = analyses => (branch: string, dottedName: DottedName) => { - let i = getBranchIndex(branch) - return getRuleFromAnalysis(analyses[i])(dottedName) -} +import { EngineContext } from './utils/EngineContext' type SchemeComparaisonProps = { hideAutoEntrepreneur?: boolean @@ -45,27 +35,50 @@ export default function SchemeComparaison({ hideAssimiléSalarié = false }: SchemeComparaisonProps) { const dispatch = useDispatch() - dispatch(setSimulationConfig(ComparaisonConfig)) - - const analyses = useSelector(analysisWithDefaultsSelector) - const plafondAutoEntrepreneurDépassé = useSelector((state: RootState) => - branchAnalyseSelector(state, { - situationBranchName: 'Auto-entrepreneur' - }).controls?.find( + dispatch(setSimulationConfig(dirigeantComparaison)) + const plafondAutoEntrepreneurDépassé = useContext(EngineContext) + .controls() + .find( ({ test }) => test.includes && test.includes('base des cotisations > plafond') ) - ) - let getRule = getRuleFrom(analyses) const [showMore, setShowMore] = useState(false) const [conversationStarted, setConversationStarted] = useState( - !!getRule('assimilé', 'revenu net après impôt')?.nodeValue + !!Object.keys(useSelector(situationSelector)).length ) const startConversation = useCallback(() => setConversationStarted(true), [ setConversationStarted ]) + const parsedRules = useContext(EngineContext).getParsedRules() + const situation = useSelector(situationSelector) + const displayResult = + useSelector(situationSelector)['entreprise . charges'] != undefined + const assimiléEngine = useMemo( + () => + new Engine(parsedRules).setSituation({ + ...situation, + dirigeant: "'assimilé salarié'" + }), + [situation] + ) + const autoEntrepreneurEngine = useMemo( + () => + new Engine(parsedRules).setSituation({ + ...situation, + dirigeant: "'auto-entrepreneur'" + }), + [situation] + ) + const indépendantEngine = useMemo( + () => + new Engine(parsedRules).setSituation({ + ...situation, + dirigeant: "'indépendant'" + }), + [situation] + ) return ( <>
    )} - {conversationStarted && ( - <> - -

    Unité

    -
    -
    - -
    - - )}
    {!conversationStarted ? ( <> @@ -325,244 +328,185 @@ export default function SchemeComparaison({
    )}
    - {conversationStarted && - !!getRule('assimilé', 'revenu net après impôt')?.nodeValue && ( - <> - -

    Revenu net après impôt

    -
    -
    - - - -
    -
    - - - -
    -
    - - {plafondAutoEntrepreneurDépassé ? ( - 'Plafond de CA dépassé' - ) : ( - - )} - -
    - -

    - Revenu net de cotisations (avant impôts) -

    -
    -
    - -
    -
    - -
    -
    - {plafondAutoEntrepreneurDépassé ? ( - '—' - ) : ( - - )} -
    + {displayResult && ( + <> +

    - - Pension de retraite - (avant impôts) - + Revenu net de cotisations (avant impôts)

    -
    - - +
    + +
    +
    + +
    +
    + <> + {plafondAutoEntrepreneurDépassé && 'Plafond de CA dépassé'} + + +
    +

    + + Pension de retraite + (avant impôts) + +

    +
    + {' '} + + + Pension calculée pour 172 trimestres cotisés au régime général + sans variations de revenus. + + +
    +
    + {' '} + + + Pension calculée pour 172 trimestres cotisés au régime des + indépendants sans variations de revenus. + + +
    +
    + {plafondAutoEntrepreneurDépassé ? ( + '—' + ) : ( + <> + {' '} - - Pension calculée pour 172 trimestres cotisés au régime - général sans variations de revenus. + + Pension calculée pour 172 trimestres cotisés en + auto-entrepreneur sans variations de revenus. - -
    -
    - {getRule('indépendant', 'protection sociale . retraite') - .isApplicable !== false ? ( - - {' '} - - - Pension calculée pour 172 trimestres cotisés au régime - des indépendants sans variations de revenus. - - - - ) : ( - - Pas implémenté - - )} -
    -
    - {plafondAutoEntrepreneurDépassé ? ( - '—' - ) : getRule( - 'auto-entrepreneur', - 'protection sociale . retraite' - ).isApplicable !== false ? ( - - {' '} - - - Pension calculée pour 172 trimestres cotisés en - auto-entrepreneur sans variations de revenus. - - - - ) : ( - - Pas implémenté - - )} -
    - -

    - Nombre de trimestres validés (pour la retraite) -

    -
    -
    - trimestres} - unit={null} + + )} +
    + +

    + Nombre de trimestres validés (pour la retraite) +

    +
    +
    + +
    +
    + +
    +
    + {plafondAutoEntrepreneurDépassé ? ( + '—' + ) : ( + -
    -
    - trimestres} - unit={null} + )} +
    + +

    + Indemnités journalières (en cas d'arrêt maladie) +

    +
    +
    + + -
    -
    - {plafondAutoEntrepreneurDépassé ? ( - '—' - ) : ( - trimestres} - unit={null} - /> - )} -
    - -

    - Indemnités journalières{' '} - (en cas d'arrêt maladie) -

    -
    -
    + + + ( + {' '} + + pour les accidents de trajet/travail et maladie pro + + ) + +
    +
    + +
    +
    + {plafondAutoEntrepreneurDépassé ? ( + '—' + ) : ( - - / jour - - } - rule="protection sociale . santé . indemnités journalières" + - - ( - {' '} - - pour les accidents de trajet/travail et maladie pro - - ) - -
    -
    - - {getRule( - 'indépendant', - 'protection sociale . santé . indemnités journalières' - ).isApplicable !== false ? ( - - - / jour - - } - branch="indépendant" - rule="protection sociale . santé . indemnités journalières" - /> - - ) : ( - - Pas implémenté - - )} - -
    -
    - {plafondAutoEntrepreneurDépassé ? ( - '—' - ) : ( - - - / jour - - } - /> - - )} -
    - - )} + )} +
    + + )}

    @@ -622,35 +566,3 @@ export default function SchemeComparaison({ ) } - -type RuleValueLinkProps = { - branch: string - rule: DottedName - appendText?: React.ReactNode - unit?: null | string -} - -function RuleValueLink({ - branch, - rule: dottedName, - appendText, - unit -}: RuleValueLinkProps) { - const dispatch = useDispatch() - const analyses = useSelector(analysisWithDefaultsSelector) - const sitePaths = useContext(SitePathsContext) - let rule = getRuleFrom(analyses)(branch, dottedName) - return !rule ? null : ( - dispatch(setSituationBranch(getBranchIndex(branch)))} - to={sitePaths.documentation.rule(rule.dottedName)} - > - - {appendText && <> {appendText}} - - ) -} diff --git a/source/components/SearchBar.tsx b/source/components/SearchBar.tsx index 1ecda922f..eef6d5ec7 100644 --- a/source/components/SearchBar.tsx +++ b/source/components/SearchBar.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import { parentName } from 'Engine/ruleUtils' import { ParsedRule, ParsedRules } from 'Engine/types' import { pick, sortBy, take } from 'ramda' @@ -10,6 +10,7 @@ import { DottedName } from 'Rules' import Worker from 'worker-loader!./SearchBar.worker.js' import { capitalise0 } from '../utils' import './SearchBar.css' +import { UseDefaultValuesContext } from './Documentation/UseDefaultValuesContext' const worker = new Worker() @@ -34,15 +35,16 @@ export default function SearchBar({ let [focusElem, setFocusElem] = useState(-1) const { i18n } = useTranslation() const history = useHistory() - + const useDefaultValues = useContext(UseDefaultValuesContext) const handleKeyDown = e => { if (e.key === 'Enter' && results.length > 0) { finallyCallback && finallyCallback() - history.push( - sitePaths.documentation.rule( + history.push({ + pathname: sitePaths.documentation.rule( results[focusElem > 0 ? focusElem : 0].dottedName - ) - ) + ), + state: { useDefaultValues } + }) } if ( @@ -191,7 +193,12 @@ export default function SearchBar({ return ( <> {formattedResults.length === 0 && ( - + {title || capitalise0(name) || ''} )} @@ -202,7 +209,12 @@ export default function SearchBar({ return ( diff --git a/source/components/SearchButton.tsx b/source/components/SearchButton.tsx index 8e91b1d03..507ad9a51 100644 --- a/source/components/SearchButton.tsx +++ b/source/components/SearchButton.tsx @@ -1,9 +1,9 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useContext } from 'react' import emoji from 'react-easy-emoji' import { Trans } from 'react-i18next' import { useSelector } from 'react-redux' -import { parsedRulesSelector } from 'Selectors/analyseSelectors' import Overlay from './Overlay' +import { EngineContext } from 'Components/utils/EngineContext' import SearchBar from './SearchBar' type SearchButtonProps = { @@ -11,7 +11,7 @@ type SearchButtonProps = { } export default function SearchButton({ invisibleButton }: SearchButtonProps) { - const rules = useSelector(parsedRulesSelector) + const rules = useContext(EngineContext).getParsedRules() const [visible, setVisible] = useState(false) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { diff --git a/source/components/SimulateurWarning.tsx b/source/components/SimulateurWarning.tsx index 828304df6..6c97ce241 100644 --- a/source/components/SimulateurWarning.tsx +++ b/source/components/SimulateurWarning.tsx @@ -1,7 +1,7 @@ import Warning from 'Components/ui/WarningBlock' import React from 'react' import { Trans } from 'react-i18next' -import { SitePaths } from './utils/withSitePaths' +import { SitePaths } from './utils/SitePathsContext' type SimulateurWarningProps = { simulateur: Exclude diff --git a/source/components/Simulation.tsx b/source/components/Simulation.tsx index ed7794e37..937fb6a11 100644 --- a/source/components/Simulation.tsx +++ b/source/components/Simulation.tsx @@ -9,8 +9,8 @@ import TargetSelection from 'Components/TargetSelection' import React from 'react' import { Trans } from 'react-i18next' import { useSelector } from 'react-redux' -import { firstStepCompletedSelector } from 'Selectors/analyseSelectors' -import { simulationProgressSelector } from 'Selectors/progressSelectors' +import { firstStepCompletedSelector } from 'Selectors/simulationSelectors' +import { useSimulationProgress } from 'Components/utils/useNextQuestion' import * as Animate from 'Ui/animate' import Progress from 'Ui/Progress' @@ -28,7 +28,7 @@ export default function Simulation({ showPeriodSwitch }: SimulationProps) { const firstStepCompleted = useSelector(firstStepCompletedSelector) - const progress = useSelector(simulationProgressSelector) + const progress = useSimulationProgress() return ( <> diff --git a/source/components/TargetSelection.css b/source/components/TargetSelection.css index 282a9ed1a..ba864b014 100644 --- a/source/components/TargetSelection.css +++ b/source/components/TargetSelection.css @@ -62,7 +62,7 @@ #targetSelection .optionTitle { font-size: 115%; - font-weight: 500; + font-weight: 600; } #targetSelection .optionTitle a { color: inherit; @@ -169,7 +169,7 @@ font-style: italic; color: #c0392b; background: yellow; - font-weight: 500; + font-weight: 600; } /* Autre idée pour styler les checkboxes https://codepen.io/KenanYusuf/pen/PZKEKd */ diff --git a/source/components/TargetSelection.tsx b/source/components/TargetSelection.tsx index 5d8fca48f..8d19b9534 100644 --- a/source/components/TargetSelection.tsx +++ b/source/components/TargetSelection.tsx @@ -3,11 +3,16 @@ import InputSuggestions from 'Components/conversation/InputSuggestions' import PeriodSwitch from 'Components/PeriodSwitch' import RuleLink from 'Components/RuleLink' import { ThemeColorsContext } from 'Components/utils/colors' -import { SitePathsContext } from 'Components/utils/withSitePaths' -import { formatCurrency } from 'Engine/format' -import { ParsedRule } from 'Engine/types' -import { isEmpty, isNil } from 'ramda' -import React, { useContext, useEffect, useState } from 'react' +import { + EngineContext, + useEvaluation, + useInversionFail +} from 'Components/utils/EngineContext' +import { SitePathsContext } from 'Components/utils/SitePathsContext' +import { formatCurrency, formatValue } from 'Engine/format' +import { EvaluatedRule } from 'Engine/types' +import { isNil } from 'ramda' +import React, { useCallback, useContext, useEffect, useState } from 'react' import emoji from 'react-easy-emoji' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -15,10 +20,9 @@ import { Link, useLocation } from 'react-router-dom' import { RootState } from 'Reducers/rootReducer' import { DottedName } from 'Rules' import { - analysisWithDefaultsSelector, situationSelector, - useTarget -} from 'Selectors/analyseSelectors' + targetUnitSelector +} from 'Selectors/simulationSelectors' import Animate from 'Ui/animate' import AnimatedTargetValue from 'Ui/AnimatedTargetValue' import CurrencyInput from './CurrencyInput/CurrencyInput' @@ -26,48 +30,12 @@ import './TargetSelection.css' export default function TargetSelection({ showPeriodSwitch = true }) { const [initialRender, setInitialRender] = useState(true) - const analysis = useSelector(analysisWithDefaultsSelector) const objectifs = useSelector( (state: RootState) => state.simulation?.config.objectifs || [] ) - const secondaryObjectives = useSelector( - (state: RootState) => - state.simulation?.config['objectifs secondaires'] || [] - ) - const situation = useSelector(situationSelector) - const dispatch = useDispatch() const colors = useContext(ThemeColorsContext) - const targets = - analysis?.targets.filter( - t => - !secondaryObjectives.includes(t.dottedName) && - t.dottedName !== 'contrat salarié . aides employeur' - ) || [] - useEffect(() => { - // Initialize defaultValue for target that can't be computed - // TODO: this logic shouldn't be here - targets - .filter( - target => - (!target.formule || isEmpty(target.formule)) && - (!isNil(target.defaultValue) || - !isNil(target.explanation?.defaultValue)) && - !situation[target.dottedName] - ) - - .forEach(target => { - dispatch( - updateSituation( - target.dottedName, - !isNil(target.defaultValue) - ? target.defaultValue - : target.explanation?.defaultValue - ) - ) - }) - setInitialRender(false) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -77,7 +45,7 @@ export default function TargetSelection({ showPeriodSwitch = true }) { {((typeof objectifs[0] === 'string' ? [{ objectifs }] : objectifs) as any).map( - ({ icône, objectifs: groupTargets, nom }, index) => ( + ({ icône, objectifs: targets, nom }, index) => (
    @@ -101,14 +69,16 @@ export default function TargetSelection({ showPeriodSwitch = true }) { )` }} > - - groupTargets.includes(dottedName) - ), - initialRender - }} - /> +
      + {' '} + {targets.map(target => ( + + ))} +
    ) @@ -117,36 +87,25 @@ export default function TargetSelection({ showPeriodSwitch = true }) { ) } -let Targets = ({ targets, initialRender }) => ( -
    -
      - {targets - .map(target => target.explanation || target) - .filter(target => { - return ( - target.isApplicable !== false && - (target.question || target.nodeValue) - ) - }) - .map(target => ( - - ))} -
    -
    -) - -const Target = ({ target, initialRender }) => { +type TargetProps = { + dottedName: DottedName + initialRender: boolean +} +const Target = ({ dottedName, initialRender }: TargetProps) => { const activeInput = useSelector((state: RootState) => state.activeTargetInput) const dispatch = useDispatch() - - const isActiveInput = activeInput === target.dottedName + const target = useEvaluation(dottedName, { + unit: useSelector(targetUnitSelector) + }) const isSmallTarget = !!target.question !== !!target.formule + if ( + target.nodeValue === false || + (isSmallTarget && !target.question && !target.nodeValue) + ) { + return null + } + const isActiveInput = activeInput === target.dottedName + return (
  • { onFirstClick={value => { dispatch(updateSituation(target.dottedName, value)) }} - unit={target.defaultUnit} + unit={target.unit} />
  • @@ -209,7 +168,12 @@ let Header = ({ target }) => { - + {target.title || target.name} {hackyShowPeriod && ' ' + t('mensuel')} @@ -221,7 +185,7 @@ let Header = ({ target }) => { } type TargetInputOrValueProps = { - target: ParsedRule + target: EvaluatedRule isActiveInput: boolean isSmallTarget: boolean } @@ -235,16 +199,29 @@ function TargetInputOrValue({ const colors = useContext(ThemeColorsContext) const dispatch = useDispatch() const situationValue = useSelector(situationSelector)[target.dottedName] - - const targetWithValue = useTarget(target.dottedName) - const inversionFail = useSelector(analysisWithDefaultsSelector)?.cache._meta - .inversionFail + const targetUnit = useSelector(targetUnitSelector) + const engine = useContext(EngineContext) const value = - targetWithValue?.nodeValue != null && !inversionFail - ? Math.round(targetWithValue.nodeValue) + typeof situationValue === 'string' + ? Math.round( + engine.evaluate(situationValue, { unit: targetUnit }) + .nodeValue as number + ) + : situationValue != null + ? situationValue + : target?.nodeValue != null + ? Math.round(+target.nodeValue) : undefined - const blurValue = inversionFail && !isActiveInput + const blurValue = useInversionFail() && !isActiveInput + + const onChange = useCallback( + evt => + dispatch( + updateSituation(target.dottedName, +evt.target.value + ' ' + targetUnit) + ), + [targetUnit, target, dispatch] + ) return ( - dispatch( - updateSituation(target.dottedName, Number(evt.target.value)) - ) + isActiveInput || + isNil(value) || + (target.question && isSmallTarget) + ? 'targetInput' + : 'editableTarget' } + onChange={onChange} onFocus={() => { if (isSmallTarget) return dispatch(setActiveTarget(target.dottedName)) @@ -292,8 +269,10 @@ function TargetInputOrValue({ ) } function TitreRestaurant() { - const titresRestaurant = useTarget( - 'contrat salarié . frais professionnels . titres-restaurant . montant' + const targetUnit = useSelector(targetUnitSelector) + const titresRestaurant = useEvaluation( + 'contrat salarié . frais professionnels . titres-restaurant . montant', + { unit: targetUnit } ) const { language } = useTranslation().i18n if (!titresRestaurant?.nodeValue) return null @@ -303,7 +282,11 @@ function TitreRestaurant() { +{' '} - {formatCurrency(titresRestaurant.nodeValue, language)} + {formatValue({ + nodeValue: titresRestaurant.nodeValue, + unit: '€', + language + })} {' '} en titres-restaurant {emoji(' 🍽')} @@ -312,16 +295,18 @@ function TitreRestaurant() { ) } function AidesGlimpse() { - const aides = useTarget('contrat salarié . aides employeur') + const targetUnit = useSelector(targetUnitSelector) + const aides = useEvaluation('contrat salarié . aides employeur', { + unit: targetUnit + }) const { language } = useTranslation().i18n // Dans le cas où il n'y a qu'une seule aide à l'embauche qui s'applique, nous // faisons un lien direct vers cette aide, plutôt qu'un lien vers la liste qui // est une somme des aides qui sont toutes nulle sauf l'aide active. - const aidesNode = aides?.explanation - const aidesDetail = aides?.explanation.formule.explanation.explanation + const aidesDetail = aides?.formule.explanation.explanation const aidesNotNul = aidesDetail?.filter(node => node.nodeValue !== false) - const aideLink = aidesNotNul?.length === 1 ? aidesNotNul[0] : aidesNode + const aideLink = aidesNotNul?.length === 1 ? aidesNotNul[0] : aides if (!aides?.nodeValue) return null return ( @@ -330,11 +315,15 @@ function AidesGlimpse() { en incluant{' '} - - {formatCurrency(aides.nodeValue, language)} - + + {formatValue({ + nodeValue: aides.nodeValue, + unit: '€', + language + })} + {' '} - d'aides {emoji(aides.explanation?.icons ?? '')} + d'aides {emoji(aides?.icons ?? '')}
    diff --git a/source/components/Targets.css b/source/components/Targets.css deleted file mode 100644 index 4f479c1d8..000000000 --- a/source/components/Targets.css +++ /dev/null @@ -1,23 +0,0 @@ -#targets { - width: 100%; - display: flex; - justify-content: center; - align-items: center; -} - -#targets > .icon { - margin: 0 0.6em; - font-size: 200%; - color: var(--color); -} -#targets .value { - font-size: 180%; -} -#targets .unit { -} - -#targets .explanation { - font-size: 150%; - text-decoration: none; - line-height: 0; -} diff --git a/source/components/Targets.tsx b/source/components/Targets.tsx deleted file mode 100644 index 3b6f183d6..000000000 --- a/source/components/Targets.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { ThemeColorsContext } from 'Components/utils/colors' -import { SitePathsContext } from 'Components/utils/withSitePaths' -import React, { useContext } from 'react' -import emoji from 'react-easy-emoji' -import { useSelector } from 'react-redux' -import { Link } from 'react-router-dom' -import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' -import './Targets.css' - -export default function Targets() { - const colors = useContext(ThemeColorsContext) - const sitePaths = useContext(SitePathsContext) - const analysis = useSelector(analysisWithDefaultsSelector) - let { nodeValue, unité: unit, dottedName } = analysis.targets[0] - return ( -
    - - - - {nodeValue?.toFixed(1)}{' '} - {unit} - - - {emoji('📖')} - - -
    - ) -} diff --git a/source/components/Value.tsx b/source/components/Value.tsx deleted file mode 100644 index 515a35ac7..000000000 --- a/source/components/Value.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import classnames from 'classnames' -import { formatValue, formatValueOptions } from 'Engine/format' -import { EvaluatedRule } from 'Engine/types' -import { Unit } from 'Engine/units' -import React from 'react' -import { Trans, useTranslation } from 'react-i18next' - -// let booleanTranslations = { true: '✅', false: '❌' } - -let booleanTranslations = { - fr: { true: 'Oui', false: 'Non' }, - en: { true: 'Yes', false: 'No' } -} - -let style = customStyle => ` - font-family: 'Courier New', Courier, monospace; - - ${customStyle} -` - -export type ValueProps = Partial< - Pick & - Pick< - formatValueOptions, - 'maximumFractionDigits' | 'minimumFractionDigits' - > & { - nilValueSymbol: string - children: number - negative: boolean - unit: string | Unit - customCSS: string - className?: string - } -> - -export default function Value({ - nodeValue: value, - unit, - nilValueSymbol, - maximumFractionDigits, - minimumFractionDigits, - children, - className, - negative, - customCSS = '' -}: ValueProps) { - const { language } = useTranslation().i18n - - /* Either an entire rule object is passed, or just the right attributes and the value as a JSX child*/ - let nodeValue = value === undefined ? children : value - - if ( - (nilValueSymbol !== undefined && nodeValue === 0) || - (nodeValue && Number.isNaN(nodeValue)) || - nodeValue === null - ) - return ( - - - - - ) - let valueType = typeof nodeValue, - formattedValue = - valueType === 'string' ? ( - {nodeValue} - ) : valueType === 'object' ? ( - (nodeValue as any).nom - ) : valueType === 'boolean' ? ( - booleanTranslations[language][nodeValue] - ) : nodeValue !== undefined ? ( - formatValue({ - minimumFractionDigits, - maximumFractionDigits, - language, - unit, - value: nodeValue - }) - ) : null - return nodeValue == undefined ? null : ( - - {negative ? '-' : ''} - {formattedValue} - - ) -} diff --git a/source/components/conversation/Aide.tsx b/source/components/conversation/Aide.tsx index aedf52664..29f8a664e 100644 --- a/source/components/conversation/Aide.tsx +++ b/source/components/conversation/Aide.tsx @@ -1,16 +1,16 @@ import { explainVariable } from 'Actions/actions' import Overlay from 'Components/Overlay' import { Markdown } from 'Components/utils/markdown' -import React from 'react' +import React, { useContext } from 'react' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' -import { parsedRulesSelector } from 'Selectors/analyseSelectors' -import References from '../rule/References' +import References from '../Documentation/References' import './Aide.css' +import { EngineContext } from 'Components/utils/EngineContext' export default function Aide() { const explained = useSelector((state: RootState) => state.explainedVariable) - const rules = useSelector(parsedRulesSelector) + const rules = useContext(EngineContext).getParsedRules() const dispatch = useDispatch() const stopExplaining = () => dispatch(explainVariable()) @@ -29,9 +29,7 @@ export default function Aide() { `} >

    {rule.title}

    -

    - -

    + {refs && (
    diff --git a/source/components/conversation/AnswerList.tsx b/source/components/conversation/AnswerList.tsx index 40e9cb200..3ce6972d1 100644 --- a/source/components/conversation/AnswerList.tsx +++ b/source/components/conversation/AnswerList.tsx @@ -1,18 +1,14 @@ import { goToQuestion, resetSimulation } from 'Actions/actions' import Overlay from 'Components/Overlay' -import Value from 'Components/Value' -import { getRuleFromAnalysis } from 'Engine/ruleUtils' +import { useEvaluation } from 'Components/utils/EngineContext' +import { useNextQuestions } from 'Components/utils/useNextQuestion' +import { formatValue } from 'Engine/format' import React from 'react' import emoji from 'react-easy-emoji' -import { Trans } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { RootState } from 'Reducers/rootReducer' -import { createSelector } from 'reselect' -import { - analysisWithDefaultsSelector, - nextStepsSelector -} from 'Selectors/analyseSelectors' -import { softCatch } from '../../utils' +import { DottedName } from 'Rules' +import { answeredQuestionsSelector } from 'Selectors/simulationSelectors' import './AnswerList.css' type AnswerListProps = { @@ -21,46 +17,60 @@ type AnswerListProps = { export default function AnswerList({ onClose }: AnswerListProps) { const dispatch = useDispatch() - const { folded, next } = useSelector(stepsToRules) + const answeredQuestions = useSelector(answeredQuestionsSelector) + const nextSteps = useNextQuestions() + return ( -

    - {emoji('📋 ')} - Mes réponses - - {emoji('🗑')}{' '} - - -

    - - {next.length > 0 && ( + {!!answeredQuestions.length && ( + <> +

    + {emoji('📋 ')} + Mes réponses + + {emoji('🗑')}{' '} + + +

    + + + )} + {!!nextSteps.length && ( <>

    {emoji('🔮 ')} Prochaines questions

    - + )}
    ) } -function StepsTable({ rules, onClose }) { +function StepsTable({ + rules, + onClose +}: { + rules: Array + onClose: () => void +}) { const dispatch = useDispatch() + const evaluatedRules = useEvaluation(rules) + const language = useTranslation().i18n.language return ( - {rules - .filter(rule => rule.nodeValue !== undefined) + {evaluatedRules + .filter(rule => rule.isApplicable !== false) .map(rule => ( span { border-bottom-color: var(--textColorOnWhite); padding: 0.05em 0em; @@ -98,7 +108,7 @@ function StepsTable({ rules, onClose }) { `} > - + {formatValue({ ...rule, language })} {' '} @@ -108,17 +118,3 @@ function StepsTable({ rules, onClose }) {
    ) } - -const stepsToRules = createSelector( - (state: RootState) => state.simulation?.foldedSteps || [], - nextStepsSelector, - analysisWithDefaultsSelector, - (folded, nextSteps, analysis) => ({ - folded: folded - .map(softCatch(getRuleFromAnalysis(analysis))) - .filter(Boolean), - next: nextSteps - .map(softCatch(getRuleFromAnalysis(analysis))) - .filter(Boolean) - }) -) diff --git a/source/components/conversation/Conversation.tsx b/source/components/conversation/Conversation.tsx index ef5f8adfc..9e36f1016 100644 --- a/source/components/conversation/Conversation.tsx +++ b/source/components/conversation/Conversation.tsx @@ -1,20 +1,22 @@ import { goToQuestion, validateStepWithValue } from 'Actions/actions' import QuickLinks from 'Components/QuickLinks' import RuleInput from 'Engine/RuleInput' -import React from 'react' +import React, { useContext, useEffect } from 'react' import emoji from 'react-easy-emoji' import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' -import { - currentQuestionSelector, - nextStepsSelector, - parsedRulesSelector -} from 'Selectors/analyseSelectors' import * as Animate from 'Ui/animate' import Aide from './Aide' import './conversation.css' import FormDecorator from './FormDecorator' +import { useNextQuestions } from 'Components/utils/useNextQuestion' +import { EngineContext } from 'Components/utils/EngineContext' +import PreviousAnswers from 'sites/mon-entreprise.fr/pages/Créer/GuideStatut/PreviousAnswers' +import { + answeredQuestionsSelector, + currentQuestionSelector +} from 'Selectors/simulationSelectors' export type ConversationProps = { customEndMessages?: React.ReactNode @@ -22,18 +24,15 @@ export type ConversationProps = { export default function Conversation({ customEndMessages }: ConversationProps) { const dispatch = useDispatch() - const rules = useSelector(parsedRulesSelector) - const currentQuestion = useSelector(currentQuestionSelector) - const previousAnswers = useSelector( - (state: RootState) => state.simulation?.foldedSteps || [] - ) - const nextSteps = useSelector(nextStepsSelector) + const rules = useContext(EngineContext).getParsedRules() + const currentQuestion = useNextQuestions()[0] + const previousAnswers = useSelector(answeredQuestionsSelector) const setDefault = () => dispatch( validateStepWithValue( currentQuestion, - rules[currentQuestion].defaultValue + rules[currentQuestion]['par défaut'] ) ) const goToPrevious = () => @@ -45,35 +44,31 @@ export default function Conversation({ customEndMessages }: ConversationProps) { } const DecoratedInputComponent = FormDecorator(RuleInput) - return rules && nextSteps.length ? ( + return currentQuestion ? ( <>
    - {currentQuestion && ( - - - - -
    - {previousAnswers.length > 0 && ( - <> - - - )} + + + +
    + {previousAnswers.length > 0 && ( + <> -
    - - )} + + )} + +
    diff --git a/source/components/conversation/Explicable.tsx b/source/components/conversation/Explicable.tsx index b614f2cb5..81ad54af1 100644 --- a/source/components/conversation/Explicable.tsx +++ b/source/components/conversation/Explicable.tsx @@ -4,15 +4,15 @@ import emoji from 'react-easy-emoji' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' import { DottedName } from 'Rules' -import { parsedRulesSelector } from 'Selectors/analyseSelectors' import { TrackerContext } from '../utils/withTracker' import './Explicable.css' +import { EngineContext } from 'Components/utils/EngineContext' export default function Explicable({ dottedName }: { dottedName: DottedName }) { + const rules = useContext(EngineContext).getParsedRules() const tracker = useContext(TrackerContext) const dispatch = useDispatch() const explained = useSelector((state: RootState) => state.explainedVariable) - const rules = useSelector(parsedRulesSelector) // Rien à expliquer ici, ce n'est pas une règle if (dottedName == null) return null diff --git a/source/components/conversation/FormDecorator.tsx b/source/components/conversation/FormDecorator.tsx index 4137fb5ea..d7deb35b3 100644 --- a/source/components/conversation/FormDecorator.tsx +++ b/source/components/conversation/FormDecorator.tsx @@ -1,12 +1,9 @@ -import { updateSituation } from 'Actions/actions' +import { updateSituation, goToQuestion } from 'Actions/actions' import Explicable from 'Components/conversation/Explicable' -import React from 'react' -import { useTranslation } from 'react-i18next' +import React, { useContext } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { - parsedRulesSelector, - situationSelector -} from 'Selectors/analyseSelectors' +import { situationSelector } from 'Selectors/simulationSelectors' +import { EngineContext } from 'Components/utils/EngineContext' /* This higher order component wraps "Form" components (e.g. Question.js), that represent user inputs, @@ -20,9 +17,8 @@ export default function FormDecorator(RenderField) { return function FormStep({ dottedName }) { const dispatch = useDispatch() const situation = useSelector(situationSelector) - const rules = useSelector(parsedRulesSelector) + const rules = useContext(EngineContext).getParsedRules() - const language = useTranslation().i18n.language const submit = source => dispatch({ type: 'STEP_ACTION', @@ -31,6 +27,7 @@ export default function FormDecorator(RenderField) { source }) const setFormValue = value => { + dispatch(goToQuestion(dottedName)) dispatch(updateSituation(dottedName, value)) } diff --git a/source/components/conversation/GroupTitle.js b/source/components/conversation/GroupTitle.js deleted file mode 100644 index 32fdd41f7..000000000 --- a/source/components/conversation/GroupTitle.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' - -/* Simple way for a visual stack : using two h1, -hinting at the fact that it is a group result */ -export default ({ - text, - onClick, - folded, - themeColors: { color, textColorOnWhite } -}) => ( -
    - {folded && ( -

    - {text} -

    - )} -

    - {text} -

    -
    -) diff --git a/source/components/conversation/Input.js b/source/components/conversation/Input.js index b515c0f15..64a846358 100644 --- a/source/components/conversation/Input.js +++ b/source/components/conversation/Input.js @@ -33,6 +33,7 @@ export default function Input({ onChange(value) }} onSecondClick={() => onSubmit && onSubmit('suggestion')} + unit={unit} />
    @@ -41,7 +42,7 @@ export default function Input({ autoFocus={autoFocus} className="suffixed" id={'step-' + dottedName} - placeholder={defaultValue} + placeholder={defaultValue?.nodeValue ?? defaultValue} thousandSeparator={thousandSeparator} decimalSeparator={decimalSeparator} allowEmptyFormatting={true} diff --git a/source/components/conversation/InputSuggestions.tsx b/source/components/conversation/InputSuggestions.tsx index f0e949e61..75290bdcc 100644 --- a/source/components/conversation/InputSuggestions.tsx +++ b/source/components/conversation/InputSuggestions.tsx @@ -1,15 +1,12 @@ -import { Rule } from 'Engine/types' import { toPairs } from 'ramda' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' -import { defaultUnitSelector } from 'Selectors/analyseSelectors' -import { convertUnit, parseUnit, Unit } from '../../engine/units' +import { serializeUnit, Unit } from '../../engine/units' type InputSuggestionsProps = { - suggestions?: Rule['suggestions'] - onFirstClick: (val: number | string) => void - onSecondClick?: (val: number | string) => void + suggestions?: Record + onFirstClick: (val: string) => void + onSecondClick?: (val: string) => void unit?: Unit } @@ -21,27 +18,25 @@ export default function InputSuggestions({ }: InputSuggestionsProps) { const [suggestion, setSuggestion] = useState() const { t } = useTranslation() - const defaultUnit = parseUnit(useSelector(defaultUnitSelector) ?? '') if (!suggestions) return null return (
    Suggestions : - {toPairs(suggestions).map(([text, value]: [string, string | number]) => { - value = - unit && typeof value === 'number' - ? convertUnit(unit, defaultUnit, value) - : value + {toPairs(suggestions).map(([text, value]: [string, number]) => { + const valueWithUnit: string = `${value} ${ + unit ? serializeUnit(unit)?.replace(' / ', '/') : '' + }` return ( - )} + {showAnswerModal && setShowAnswerModal(false)} />} ) diff --git a/source/components/conversation/select/SelectGéo.js b/source/components/conversation/select/SelectGeo.js similarity index 94% rename from source/components/conversation/select/SelectGéo.js rename to source/components/conversation/select/SelectGeo.js index 177cc0762..1232d1d78 100644 --- a/source/components/conversation/select/SelectGéo.js +++ b/source/components/conversation/select/SelectGeo.js @@ -47,16 +47,14 @@ export default function Select({ onChange, onSubmit }) { tauxVersementTransport(option.code) .then(({ taux }) => { // serialize to not mix our data schema and the API response's - onChange( - JSON.stringify({ - ...option, - ...(taux != undefined - ? { - 'taux du versement transport': taux - } - : {}) - }) - ) + onChange({ + ...option, + ...(taux != undefined + ? { + 'taux du versement transport': taux + } + : {}) + }) onSubmit() }) .catch(error => { diff --git a/source/components/rule/Examples.js b/source/components/rule/Examples.js deleted file mode 100644 index 805e8344d..000000000 --- a/source/components/rule/Examples.js +++ /dev/null @@ -1,79 +0,0 @@ -import { setExample } from 'Actions/actions' -import classNames from 'classnames' -import { compose } from 'ramda' -import React from 'react' -import { Trans } from 'react-i18next' -import { connect } from 'react-redux' - -export default compose( - connect( - state => ({ - parsedRules: state.parsedRules, - themeColors: state.themeColors - }), - dispatch => ({ - setExample: compose(dispatch, setExample) - }) - ) -)(function Examples({ - situationExists, - rule, - themeColors, - setExample, - currentExample -}) { - let { examples } = rule - - if (!examples) return null - return ( - <> -

    - Exemples{' '} - - - Cliquez sur un exemple pour le tester - - -

    -
      - {examples.map(ex => ( - - ))} -
    - - {situationExists && currentExample && ( - - )} - - ) -}) - -let Example = ({ - ex: { nom, situation }, - rule, - currentExample, - setExample -}) => { - let selected = currentExample && currentExample.name == nom - return ( -
  • - -
  • - ) -} diff --git a/source/components/rule/Rule.css b/source/components/rule/Rule.css deleted file mode 100644 index 2bfa6c063..000000000 --- a/source/components/rule/Rule.css +++ /dev/null @@ -1,31 +0,0 @@ -.reportErrorContainer { - text-align: center; - padding: 0.3em 0.6em; - margin: 3em auto 0; -} -.reportError { - color: #c0392b; - font-size: 100%; - text-align: center; -} -.reportError:hover { - color: #b53527; -} - -#notes { - color: #666; -} - -h2 small { - font-size: 75%; -} - - -#rule #ruleDefault { - text-align: center; - opacity: 0.75; -} - -#toggleRuleSource { - margin-top: 1em; -} diff --git a/source/components/rule/Rule.js b/source/components/rule/Rule.js deleted file mode 100644 index cb70489e2..000000000 --- a/source/components/rule/Rule.js +++ /dev/null @@ -1,239 +0,0 @@ -import { ThemeColorsContext } from 'Components/utils/colors' -import { SitePathsContext } from 'Components/utils/withSitePaths' -import Value from 'Components/Value' -import mecanisms from 'Engine/mecanisms.yaml' -import { filter, isEmpty } from 'ramda' -import React, { Suspense, useContext, useState } from 'react' -import emoji from 'react-easy-emoji' -import { Helmet } from 'react-helmet' -import { Trans, useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' -import { Link } from 'react-router-dom' -import { - exampleAnalysisSelector, - noUserInputSelector, - parsedRulesSelector, - ruleAnalysisSelector -} from 'Selectors/analyseSelectors' -import Animate from 'Ui/animate' -import { AttachDictionary } from '../AttachDictionary' -import RuleLink from '../RuleLink' -import { Markdown } from '../utils/markdown' -import Algorithm from './Algorithm' -import Examples from './Examples' -import RuleHeader from './Header' -import References from './References' -import './Rule.css' - -let LazySource = React.lazy(() => import('./RuleSource')) - -export default AttachDictionary(mecanisms)(function Rule({ dottedName }) { - const currentExample = useSelector(state => state.currentExample) - const rules = useSelector(parsedRulesSelector) - const valuesToShow = !useSelector(noUserInputSelector) - const analysedRule = useSelector(state => - ruleAnalysisSelector(state, { dottedName }) - ) - const analysedExample = useSelector(state => - exampleAnalysisSelector(state, { dottedName }) - ) - const sitePaths = useContext(SitePathsContext) - const [viewSource, setViewSource] = useState(false) - const { t } = useTranslation() - - let rule = rules[dottedName] - let { type, name, acronyme, title, description, question, icon } = rule, - namespaceRules = filter( - rule => - rule.dottedName.startsWith(dottedName) && - rule.dottedName.split(' . ').length === - dottedName.split(' . ').length + 1, - rules - ) - let displayedRule = analysedExample || analysedRule - const renderToggleSourceButton = () => { - return ( - - ) - } - - const renderReferences = ({ références: refs }) => - refs ? ( -
    -

    - Références -

    - -
    - ) : null - - return ( - <> - {viewSource ? ( - <> - {renderToggleSourceButton()} - Chargement du code source...
    }> - - - - ) : ( -
    - - - - -
    -
    .value { - font-size: 220%; - } - - margin: 0.6em 0; - > * { - margin: 0 0.6em; - } - `} - > - parent?.nodeValue == false - )} - /> -
    - {displayedRule.defaultValue != null && ( -
    - par défaut :{' '} - -
    - )} - {!valuesToShow && ( -
    - - Faire une simulation - -
    - )} - - {displayedRule['rend non applicable'] && ( -
    -

    - Rend non applicable les règles suivantes :{' '} -

    -
      - {displayedRule['rend non applicable'].map(ruleName => ( -
    • - -
    • - ))} -
    -
    - )} - {rule.note && ( -
    -

    Note :

    - -
    - )} - - {!isEmpty(namespaceRules) && ( - - )} - {renderReferences(rule)} -
    - {renderToggleSourceButton()} -
    -
    - )} - - ) -}) - -function NamespaceRulesList({ namespaceRules }) { - const colors = useContext(ThemeColorsContext) - const sitePaths = useContext(SitePathsContext) - return ( -
    -

    - Pages associées -

    -
      - {Object.values(namespaceRules).map(r => ( -
    • - - {r.title || r.name} - -
    • - ))} -
    -
    - ) -} diff --git a/source/components/rule/ShowValuesContext.tsx b/source/components/rule/ShowValuesContext.tsx deleted file mode 100644 index 1fc55612a..000000000 --- a/source/components/rule/ShowValuesContext.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' - -export const ShowValuesContext = React.createContext(false) - -const { - Consumer: ShowValuesConsumer, - Provider: ShowValuesProvider -} = ShowValuesContext -export { ShowValuesConsumer, ShowValuesProvider } diff --git a/source/components/simulationConfigs/artiste-auteur.yaml b/source/components/simulationConfigs/artiste-auteur.yaml index c9d5494df..dc05be6ce 100644 --- a/source/components/simulationConfigs/artiste-auteur.yaml +++ b/source/components/simulationConfigs/artiste-auteur.yaml @@ -1,5 +1,5 @@ situation: - dirigeant: artiste-auteur + dirigeant: "'artiste-auteur'" unité par défaut: €/an objectifs: - artiste-auteur . cotisations diff --git a/source/components/simulationConfigs/assimilé.yaml b/source/components/simulationConfigs/assimilé.yaml index c7ea9640b..ade5295f8 100644 --- a/source/components/simulationConfigs/assimilé.yaml +++ b/source/components/simulationConfigs/assimilé.yaml @@ -36,5 +36,5 @@ questions: unité par défaut: €/an situation: - dirigeant: 'assimilé salarié' + dirigeant: "'assimilé salarié'" contrat salarié . ATMP . taux réduit: oui diff --git a/source/components/simulationConfigs/auto-entrepreneur.yaml b/source/components/simulationConfigs/auto-entrepreneur.yaml index 3ed92ae1f..d722fb76e 100644 --- a/source/components/simulationConfigs/auto-entrepreneur.yaml +++ b/source/components/simulationConfigs/auto-entrepreneur.yaml @@ -18,4 +18,4 @@ questions: unité par défaut: €/an situation: - dirigeant: 'auto-entrepreneur' + dirigeant: "'auto-entrepreneur'" diff --git a/source/components/simulationConfigs/chômage-partiel.yaml b/source/components/simulationConfigs/chômage-partiel.yaml index d272d6c3c..a580649fb 100644 --- a/source/components/simulationConfigs/chômage-partiel.yaml +++ b/source/components/simulationConfigs/chômage-partiel.yaml @@ -1,12 +1,11 @@ objectifs: - contrat salarié . rémunération . brut de base -objectifs secondaires: - - contrat salarié . prix du travail +objectifs cachés: - contrat salarié . rémunération . net + - contrat salarié . prix du travail - chômage partiel . revenu net habituel - chômage partiel . coût employeur habituel - - contrat salarié . activité partielle . indemnités questions: uniquement: diff --git a/source/components/simulationConfigs/indépendant.yaml b/source/components/simulationConfigs/indépendant.yaml index a167b3549..31a35d6dc 100644 --- a/source/components/simulationConfigs/indépendant.yaml +++ b/source/components/simulationConfigs/indépendant.yaml @@ -31,4 +31,4 @@ questions: unité par défaut: €/an situation: - dirigeant: 'indépendant' + dirigeant: "'indépendant'" diff --git a/source/components/simulationConfigs/rémunération-dirigeant.yaml b/source/components/simulationConfigs/rémunération-dirigeant.yaml index 512437a8b..e6959faca 100644 --- a/source/components/simulationConfigs/rémunération-dirigeant.yaml +++ b/source/components/simulationConfigs/rémunération-dirigeant.yaml @@ -2,12 +2,12 @@ titre: | Calcul du revenu du travailleur indépendant ou dirigeant d'entreprise après paiement des cotisations et de l'impôt sur le revenu. objectifs: - - revenu net après impôt - - revenus net de cotisations + - contrat salarié . rémunération . net + - dirigeant . indépendant . revenu net de cotisations + - dirigeant . auto-entrepreneur . net de cotisations - protection sociale . retraite - - protection sociale . retraite . trimestres validés par an + - protection sociale . retraite . trimestres validés - protection sociale . santé . indemnités journalières - - protection sociale . accidents du travail et maladies professionnelles questions: uniquement: @@ -19,14 +19,6 @@ questions: - entreprise . catégorie d'activité . libérale règlementée unité par défaut: €/an -branches: - - nom: Assimilé salarié - situation: - dirigeant: 'assimilé salarié' - contrat salarié . ATMP . taux réduit: oui - - nom: Indépendant - situation: - dirigeant: 'indépendant' - - nom: Auto-entrepreneur - situation: - dirigeant: 'auto-entrepreneur' +situation: + dirigeant: "'auto-entrepreneur'" + contrat salarié . ATMP . taux réduit: oui diff --git a/source/components/simulationConfigs/salarié.yaml b/source/components/simulationConfigs/salarié.yaml index 0552d5bb5..0850d66d2 100644 --- a/source/components/simulationConfigs/salarié.yaml +++ b/source/components/simulationConfigs/salarié.yaml @@ -1,16 +1,10 @@ objectifs: - contrat salarié . prix du travail - - contrat salarié . aides employeur - contrat salarié . rémunération . brut de base . équivalent temps plein - contrat salarié . rémunération . brut de base - contrat salarié . rémunération . net - contrat salarié . rémunération . net après impôt -objectifs secondaires: - - contrat salarié . temps de travail - - contrat salarié . cotisations - - contrat salarié . frais professionnels . titres-restaurant . montant - questions: à l'affiche: Chômage partiel: contrat salarié . activité partielle diff --git a/source/components/ui/AnimatedTargetValue.tsx b/source/components/ui/AnimatedTargetValue.tsx index 59c1b431d..b8edd01a1 100644 --- a/source/components/ui/AnimatedTargetValue.tsx +++ b/source/components/ui/AnimatedTargetValue.tsx @@ -25,7 +25,7 @@ export default function AnimatedTargetValue({ // 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?.defaultUnit + (state: RootState) => state?.simulation?.targetUnit ) const previousUnit = useRef(currentUnit) diff --git a/source/components/ui/Toggle.css b/source/components/ui/Toggle.css index 8a3687eb0..521899321 100644 --- a/source/components/ui/Toggle.css +++ b/source/components/ui/Toggle.css @@ -52,7 +52,7 @@ border: 1px dotted gray; } .ui__.toggle input[type='radio']:checked ~ .radioText { - font-weight: 500; + font-weight: 600; } .ui__.toggle input[type='radio']:checked ~ * { diff --git a/source/components/ui/Typography.css b/source/components/ui/Typography.css index 5c562c0f5..fe498dad8 100644 --- a/source/components/ui/Typography.css +++ b/source/components/ui/Typography.css @@ -123,7 +123,7 @@ a:not(:disabled):not(.button):not(.button-choice):hover { strong, b { - font-weight: 500; + font-weight: 600; } textarea { diff --git a/source/components/ui/index.css b/source/components/ui/index.css index 99ff5340c..16e1486a5 100644 --- a/source/components/ui/index.css +++ b/source/components/ui/index.css @@ -126,6 +126,7 @@ span.ui__.enumeration:not(:last-of-type)::after { .ui__.label { font-size: 85%; + line-height: initial; padding: 0.4rem 0.6rem; font-weight: bold; color: white !important; @@ -133,6 +134,10 @@ span.ui__.enumeration:not(:last-of-type)::after { border-radius: 0.3rem; text-align: center; } +.ui__.small.label { + font-size: 75%; + padding: 0.2rem 0.4rem; +} .no-scroll { overflow: hidden; diff --git a/source/components/utils/EngineContext.tsx b/source/components/utils/EngineContext.tsx new file mode 100644 index 000000000..f08bf7abd --- /dev/null +++ b/source/components/utils/EngineContext.tsx @@ -0,0 +1,52 @@ +import Engine, { EvaluationOptions } from 'Engine' +import { EvaluatedRule } from 'Engine/types' +import React, { createContext, useContext } from 'react' +import rules, { DottedName } from 'Rules' + +export const EngineContext = createContext>( + new Engine(rules) +) + +export const EngineProvider = EngineContext.Provider + +type SituationProviderProps = { + children: React.ReactNode + situation: Partial> +} +export function SituationProvider({ + children, + situation +}: SituationProviderProps) { + const engine = useContext(EngineContext) + engine.setSituation(situation) + return ( + {children} + ) +} + +export function useEvaluation( + rule: DottedName, + options?: EvaluationOptions +): EvaluatedRule +export function useEvaluation( + rule: DottedName[], + options?: EvaluationOptions +): EvaluatedRule[] +export function useEvaluation( + rule: Array | DottedName, + options?: EvaluationOptions +): Array> | EvaluatedRule { + const engine = useContext(EngineContext) + if (Array.isArray(rule)) { + return rule.map(name => engine.evaluate(name, options)) + } + return engine.evaluate(rule, options) +} + +export function useInversionFail() { + return useContext(EngineContext).inversionFail() +} + +export function useControls() { + return useContext(EngineContext).controls() +} diff --git a/source/components/utils/withSitePaths.tsx b/source/components/utils/SitePathsContext.tsx similarity index 100% rename from source/components/utils/withSitePaths.tsx rename to source/components/utils/SitePathsContext.tsx diff --git a/source/components/utils/useNextQuestion.tsx b/source/components/utils/useNextQuestion.tsx new file mode 100644 index 000000000..1250a181c --- /dev/null +++ b/source/components/utils/useNextQuestion.tsx @@ -0,0 +1,132 @@ +import { splitName } from 'Engine/ruleUtils' +import { + add, + countBy, + descend, + difference, + equals, + flatten, + head, + identity, + intersection, + keys, + last, + length, + map, + mergeWith, + negate, + pair, + pipe, + reduce, + sortBy, + sortWith, + takeWhile, + toPairs, + values, + zipWith +} from 'ramda' +import { useSelector } from 'react-redux' +import { useEvaluation } from './EngineContext' +import { + objectifsSelector, + configSelector, + answeredQuestionsSelector, + currentQuestionSelector +} from 'Selectors/simulationSelectors' +import { useMemo } from 'react' +import { DottedName } from 'Rules' +import { SimulationConfig } from 'Reducers/rootReducer' + +type MissingVariables = Array>> +export function getNextSteps( + missingVariables: MissingVariables +): Array { + let byCount = ([, [count]]) => count + let byScore = ([, [, score]]) => score + + let missingByTotalScore = reduce(mergeWith(add), {}, missingVariables) + + let innerKeys = flatten(map(keys, missingVariables)), + missingByTargetsAdvanced = countBy(identity, innerKeys) + + let missingByCompound = mergeWith( + pair, + missingByTargetsAdvanced, + missingByTotalScore + ), + pairs = toPairs(missingByCompound), + sortedPairs = sortWith([descend(byCount), descend(byScore) as any], pairs) + return map(head, sortedPairs) +} + +const similarity = (rule1: string = '', rule2: string = '') => + pipe( + zipWith(equals), + takeWhile(Boolean), + length, + negate + )(splitName(rule1), splitName(rule2)) + +export function getNextQuestions( + missingVariables: MissingVariables, + questionConfig: SimulationConfig['questions'] = {}, + answeredQuestions = [] +): Array { + const { + 'non prioritaires': notPriority = [], + uniquement: only = null, + 'liste noire': blacklist = [] + } = questionConfig + // console.log(missingVariables) + let nextSteps = difference(getNextSteps(missingVariables), answeredQuestions) + + if (only) { + nextSteps = intersection(nextSteps, [...only, ...notPriority]) + } + if (blacklist) { + nextSteps = difference(nextSteps, blacklist) + } + + const lastStep = last(answeredQuestions) + // L'ajout de la réponse permet de traiter les questions dont la réponse est "une possibilité", exemple "contrat salarié . cdd" + // lastStepWithAnswer = + // lastStep && situation[lastStep] + // ? ([lastStep, situation[lastStep]].join(' . ') as DottedName) + // : lastStep + + return sortBy( + question => + notPriority.includes(question) + ? notPriority.indexOf(question) + : similarity(question, lastStep), + + nextSteps + ) +} + +export const useNextQuestions = function(): Array { + const objectifs = useSelector(objectifsSelector) + const answeredQuestions = useSelector(answeredQuestionsSelector) + const currentQuestion = useSelector(currentQuestionSelector) + const questionsConfig = useSelector(configSelector).questions ?? {} + const missingVariables = useEvaluation(objectifs, { + useDefaultValues: false + }).map(node => node.missingVariables ?? {}) + const nextQuestions = useMemo(() => { + return getNextQuestions( + missingVariables, + questionsConfig, + answeredQuestions + ) + }, [missingVariables, questionsConfig, answeredQuestions]) + if (currentQuestion && currentQuestion !== nextQuestions[0]) { + return [currentQuestion, ...nextQuestions] + } + return nextQuestions +} + +export function useSimulationProgress(): number { + const numberQuestionAnswered = useSelector(answeredQuestionsSelector).length + const numberQuestionLeft = useNextQuestions().length + return numberQuestionAnswered / (numberQuestionAnswered + numberQuestionLeft) +} diff --git a/source/engine/RuleInput.tsx b/source/engine/RuleInput.tsx index 0afdcb65a..a0e34d68b 100644 --- a/source/engine/RuleInput.tsx +++ b/source/engine/RuleInput.tsx @@ -1,13 +1,14 @@ import Input from 'Components/conversation/Input' import Question from 'Components/conversation/Question' -import SelectGéo from 'Components/conversation/select/SelectGéo' +import SelectGéo from 'Components/conversation/select/SelectGeo' import SelectAtmp from 'Components/conversation/select/SelectTauxRisque' import SendButton from 'Components/conversation/SendButton' import CurrencyInput from 'Components/CurrencyInput/CurrencyInput' import PercentageField from 'Components/PercentageField' import ToggleSwitch from 'Components/ui/ToggleSwitch' +import { EngineContext } from 'Components/utils/EngineContext' import { ParsedRules } from 'Engine/types' -import React from 'react' +import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' import { DottedName } from 'Rules' import DateInput from '../components/conversation/DateInput' @@ -17,7 +18,7 @@ export const binaryOptionChoices = [ { value: 'oui', label: 'Oui' } ] -type Value = string | number | object | boolean +type Value = string | number | object | boolean | null export type RuleInputProps = { rules: ParsedRules dottedName: DottedName @@ -46,8 +47,9 @@ export default function RuleInput({ onSubmit }: RuleInputProps) { let rule = rules[dottedName] - let unit = rule.unit || rule.defaultUnit + let unit = rule.unit let language = useTranslation().i18n.language + let engine = useContext(EngineContext) let commonProps = { key: dottedName, @@ -100,6 +102,11 @@ export default function RuleInput({ ) } + + commonProps.value = + typeof commonProps.value === 'string' + ? engine.evaluate(commonProps.value as DottedName).nodeValue + : commonProps.value if (unit?.numerators.includes('€') && isTarget) { return ( <> @@ -123,20 +130,18 @@ export default function RuleInput({ return } -let getVariant = rule => rule?.formule?.explanation['une possibilité'] +let getVariant = rule => rule?.formule?.explanation['possibilités'] export let buildVariantTree = (allRules, path) => { let rec = path => { let node = allRules[path] if (!node) throw new Error(`La règle ${path} est introuvable`) - let variant = getVariant(node), - variants = variant && node.formule.explanation['possibilités'], - shouldBeExpanded = variant && true, //variants.find( v => relevantPaths.find(rp => contains(path + ' . ' + v)(rp) )), - canGiveUp = variant && !node.formule.explanation['choix obligatoire'] - + let variant = getVariant(node) + const variants = variant && node.formule.explanation['possibilités'] + const canGiveUp = variant && !node.formule.explanation['choix obligatoire'] return Object.assign( node, - shouldBeExpanded + !!variant ? { canGiveUp, children: variants.map(v => rec(path + ' . ' + v)) diff --git a/source/engine/error.ts b/source/engine/error.ts index 0f16855dc..eff4b1449 100644 --- a/source/engine/error.ts +++ b/source/engine/error.ts @@ -60,13 +60,13 @@ export function typeWarning( export function warning( rules: string[] | string, message: string, - solution: string + solution?: string ) { console.warn( `\n[ Avertissement ] ➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` ⚠️ ${message} -💡 ${solution} +💡 ${solution ? solution : ''} ` ) } diff --git a/source/engine/evaluateRule.ts b/source/engine/evaluateRule.ts index 7630697cc..45a4b8d3e 100644 --- a/source/engine/evaluateRule.ts +++ b/source/engine/evaluateRule.ts @@ -3,7 +3,6 @@ import { ParsedRule } from 'Engine/types' import { map, mergeAll, pick, pipe } from 'ramda' import { typeWarning } from './error' import { convertNodeToUnit } from './nodeUnits' -import { areUnitConvertible } from './units' export const evaluateApplicability = ( cache, @@ -48,8 +47,11 @@ export const evaluateApplicability = ( ]) return { + ...node, + isApplicable, nodeValue: isApplicable, missingVariables, + parentDependencies, ...evaluatedAttributes } } @@ -72,7 +74,7 @@ export default (cache, situationGate, parsedRules, node) => { ? evaluateNode(cache, situationGate, parsedRules, node.formule) : {} // evaluate the formula lazily, only if the applicability is known and true - const evaluatedFormula = isApplicable + let evaluatedFormula = isApplicable ? evaluateFormula() : isApplicable === false ? { @@ -85,27 +87,10 @@ export default (cache, situationGate, parsedRules, node) => { missingVariables: {}, nodeValue: null } - let { - missingVariables: formulaMissingVariables, - nodeValue - } = evaluatedFormula - const missingVariables = mergeMissing( - bonus(condMissing, !!Object.keys(condMissing).length), - formulaMissingVariables - ) - const unit = - node.unit || - (node.defaultUnit && - cache._meta.defaultUnits.find(unit => - areUnitConvertible(node.defaultUnit, unit) - )) || - node.defaultUnit || - evaluatedFormula.unit - const temporalValue = evaluatedFormula.temporalValue - if (unit) { + if (node.unit) { try { - nodeValue = convertNodeToUnit(unit, evaluatedFormula).nodeValue + evaluatedFormula = convertNodeToUnit(node.unit, evaluatedFormula) } catch (e) { typeWarning( node.dottedName, @@ -114,14 +99,20 @@ export default (cache, situationGate, parsedRules, node) => { ) } } + const missingVariables = mergeMissing( + bonus(condMissing, !!Object.keys(condMissing).length), + evaluatedFormula.missingVariables + ) + // console.log(node.dottedName, evaluatedFormula.unit) + let temporalValue = evaluatedFormula.temporalValue cache._meta.contextRule.pop() return { ...node, ...applicabilityEvaluation, ...(node.formule && { formule: evaluatedFormula }), - nodeValue, - unit, + nodeValue: evaluatedFormula.nodeValue, + unit: node.unit ?? evaluatedFormula.unit, temporalValue, isApplicable, missingVariables diff --git a/source/engine/evaluation.tsx b/source/engine/evaluation.tsx index 7e643d97f..5fdff9a95 100644 --- a/source/engine/evaluation.tsx +++ b/source/engine/evaluation.tsx @@ -11,9 +11,9 @@ import { import React from 'react' import { typeWarning } from './error' import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits' +import { EvaluatedNode } from 'Engine/types' import { concatTemporals, - EvaluatedNode, liftTemporalNode, mapTemporal, pureTemporal, @@ -21,10 +21,11 @@ import { temporalAverage, zipTemporals } from './temporal' +import { ParsedRule, ParsedRules } from './types' export let makeJsx = node => typeof node.jsx == 'function' - ? node.jsx(node.nodeValue, node.explanation, node.lazyEval, node.unit) + ? node.jsx(node.nodeValue, node.explanation, node.unit) : node.jsx export let collectNodeMissing = node => node.missingVariables || {} @@ -40,12 +41,6 @@ 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 } @@ -175,6 +170,7 @@ export let evaluateObject = (objectShape, effect) => ( }, temporalExplanations) const sameUnitTemporalExplanation: Temporal> = convertNodesToSameUnit( temporalExplanation.map(x => x.value), @@ -210,3 +206,22 @@ export let evaluateObject = (objectShape, effect) => ( temporalExplanation } } + +type DefaultValues = { [name in Names]: any } | {} +export function collectDefaults( + parsedRules: ParsedRules +): DefaultValues { + const cache = { _meta: { contextRule: [] as string[] } } + return (Object.values(parsedRules) as Array>).reduce( + (acc, parsedRule) => { + if (parsedRule?.['par défaut'] == null) { + return acc + } + return { + ...acc, + [parsedRule.dottedName]: parsedRule['par défaut'] + } + }, + {} + ) +} diff --git a/source/engine/format.test.js b/source/engine/format.test.js index 31a091de5..c29e1beed 100644 --- a/source/engine/format.test.js +++ b/source/engine/format.test.js @@ -1,48 +1,62 @@ import { expect } from 'chai' import { parseUnit } from 'Engine/units' -import { formatCurrency, formatPercentage, formatValue } from './format' +import { formatValue } from './format' describe('format engine values', () => { it('format currencies', () => { - expect(formatCurrency(12, 'fr')).to.equal('12 €') - expect(formatCurrency(1200, 'fr')).to.equal('1 200 €') - expect(formatCurrency(12, 'en')).to.equal('€ 12') - expect(formatCurrency(12.1)).to.equal('€ 12.10') - expect(formatCurrency(12.123)).to.equal('€ 12.12') + expect(formatValue({ nodeValue: 12, unit: '€', language: 'fr' })).to.equal( + '12 €' + ) + expect( + formatValue({ nodeValue: 1200, unit: '€', language: 'fr' }) + ).to.equal('1 200 €') + expect(formatValue({ nodeValue: 12, unit: '€', language: 'en' })).to.equal( + '€12' + ) + expect( + formatValue({ nodeValue: 12.1, unit: '€', language: 'en' }) + ).to.equal('€12.10') + expect( + formatValue({ nodeValue: 12.123, unit: '€', language: 'en' }) + ).to.equal('€12.12') }) it('format percentages', () => { - expect(formatPercentage(10)).to.equal('10%') - expect(formatPercentage(100)).to.equal('100%') - expect(formatPercentage(10.2)).to.equal('10.2%') + expect(formatValue({ nodeValue: 10, unit: '%' })).to.equal('10%') + expect(formatValue({ nodeValue: 100, unit: '%' })).to.equal('100%') + expect(formatValue({ nodeValue: 10.2, unit: '%' })).to.equal('10.2%') }) it('format values', () => { - expect(formatValue({ unit: '€', value: 12 })).to.equal('€12') - expect(formatValue({ unit: '€', value: 12.1 })).to.equal('€12.10') - expect(formatValue({ unit: '€', value: 12, language: 'fr' })).to.equal( + expect(formatValue({ unit: '€', nodeValue: 12 })).to.equal('€12') + expect(formatValue({ unit: '€', nodeValue: 12.1 })).to.equal('€12.10') + expect(formatValue({ unit: '€', nodeValue: 12, language: 'fr' })).to.equal( '12 €' ) - expect(formatValue({ value: 1200, language: 'fr' })).to.equal('1 200') + expect(formatValue({ nodeValue: 1200, language: 'fr' })).to.equal('1 200') }) }) describe('Units handling', () => { it('translate unit', () => { - expect(formatValue({ value: 1, unit: 'jour', language: 'fr' })).to.equal( - '1 jour' - ) - expect(formatValue({ value: 1, unit: 'jour', language: 'en' })).to.equal( - '1 day' - ) + expect( + formatValue({ nodeValue: 1, unit: 'jour', language: 'fr' }) + ).to.equal('1 jour') + expect( + formatValue({ nodeValue: 1, unit: 'jour', language: 'en' }) + ).to.equal('1 day') }) it('pluralize unit', () => { - expect(formatValue({ value: 2, unit: 'jour', language: 'fr' })).to.equal( - '2 jours' - ) expect( - formatValue({ value: 7, unit: parseUnit('jour/semaine'), language: 'fr' }) + formatValue({ nodeValue: 2, unit: 'jour', language: 'fr' }) + ).to.equal('2 jours') + expect( + formatValue({ + nodeValue: 7, + unit: parseUnit('jour/semaine'), + language: 'fr' + }) ).to.equal('7 jours / semaine') }) }) diff --git a/source/engine/format.ts b/source/engine/format.ts index d0049718a..684156b31 100644 --- a/source/engine/format.ts +++ b/source/engine/format.ts @@ -1,5 +1,6 @@ import { serializeUnit } from 'Engine/units' import { memoizeWith } from 'ramda' +import { Evaluation } from './types' import { Unit } from './units' const NumberFormat = memoizeWith( @@ -46,7 +47,7 @@ export const currencyFormat = (language: string | undefined) => ({ export const formatCurrency = (value: number | undefined, language: string) => { return value == null ? '' - : (formatValue({ unit: '€', language, value }) ?? '').replace( + : (formatNumber({ unit: '€', language, value }) ?? '').replace( /^(-)?€/, '$1€\u00A0' ) @@ -55,17 +56,17 @@ export const formatCurrency = (value: number | undefined, language: string) => { export const formatPercentage = (value: number | undefined) => value == null ? '' - : formatValue({ unit: '%', value, maximumFractionDigits: 2 }) + : formatNumber({ unit: '%', value, maximumFractionDigits: 2 }) export type formatValueOptions = { maximumFractionDigits?: number minimumFractionDigits?: number language?: string unit?: Unit | string - value?: number + value: number } -export function formatValue({ +function formatNumber({ maximumFractionDigits, minimumFractionDigits, language, @@ -106,3 +107,44 @@ export function formatValue({ ) } } + +const booleanTranslations = { + fr: { true: 'Oui', false: 'Non' }, + en: { true: 'Yes', false: 'No' } +} + +type ValueArg = { + nodeValue: Evaluation + language: string + unit?: string | Unit + precision?: number +} + +export function formatValue({ + nodeValue, + language, + unit, + precision = 2 +}: ValueArg) { + if ( + (typeof nodeValue === 'number' && Number.isNaN(nodeValue)) || + nodeValue === null + ) { + return '-' + } + return typeof nodeValue === 'string' + ? nodeValue + : typeof nodeValue === 'object' + ? (nodeValue as any).nom + : typeof nodeValue === 'boolean' + ? booleanTranslations[language][nodeValue] + : typeof nodeValue === 'number' + ? formatNumber({ + minimumFractionDigits: 0, + maximumFractionDigits: precision, + language, + unit, + value: nodeValue + }) + : null +} diff --git a/source/engine/generateQuestions.ts b/source/engine/generateQuestions.ts deleted file mode 100644 index f7b9ea11b..000000000 --- a/source/engine/generateQuestions.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - add, - countBy, - descend, - flatten, - head, - identity, - keys, - map, - mergeWith, - pair, - reduce, - sortWith, - toPairs, - values -} from 'ramda' -import { DottedName } from 'Rules' - -/* - COLLECTE DES VARIABLES MANQUANTES - ********************************* - on collecte les variables manquantes : celles qui sont nécessaires pour - remplir les objectifs de la simulation (calculer des cotisations) mais qui n'ont pas - encore été renseignées - - TODO perf : peut-on le faire en même temps que l'on traverse l'AST ? - Oui sûrement, cette liste se complète en remontant l'arbre. En fait, on le fait déjà pour nodeValue, - et quand nodeValue vaut null, c'est qu'il y a des missingVariables ! Il suffit donc de remplacer les - null par un tableau, et d'ailleurs utiliser des fonction d'aide pour mutualiser ces tests. - - missingVariables: {variable: [objectives]} - */ - -type Explanation = { - missingVariables: Array - dottedName: DottedName -} - -export let getNextSteps = missingVariablesByTarget => { - let byCount = ([, [count]]) => count - let byScore = ([, [, score]]) => score - - let missingByTotalScore = reduce( - mergeWith(add), - {}, - values(missingVariablesByTarget) - ) - - let innerKeys = flatten(map(keys, values(missingVariablesByTarget))), - missingByTargetsAdvanced = countBy(identity, innerKeys) - - let missingByCompound = mergeWith( - pair, - missingByTargetsAdvanced, - missingByTotalScore - ), - pairs = toPairs(missingByCompound), - sortedPairs = sortWith([descend(byCount), descend(byScore) as any], pairs) - return map(head, sortedPairs) -} diff --git a/source/engine/getSituationValue.js b/source/engine/getSituationValue.js index 8faf4b803..19b72a66f 100644 --- a/source/engine/getSituationValue.js +++ b/source/engine/getSituationValue.js @@ -21,21 +21,11 @@ let evaluateBottomUp = situationGate => startingFragments => { return rec(startingFragments) } -let formatBooleanValue = { oui: true, non: false } - export let getSituationValue = (situationGate, variableName, rule) => { // get the current situation value // it's the user input or test input, possibly with default values let value = situationGate(variableName) - if (rule.API) return typeof value == 'string' ? JSON.parse(value) : value - - if (rule.unit != null) { - return value == undefined ? value : +value - } - - // a leaf variable with an unit attribute is not boolean - if (formatBooleanValue[value] !== undefined) return formatBooleanValue[value] if (rule.formule && rule.formule['une possibilité']) return evaluateBottomUp(situationGate)(splitName(variableName)) diff --git a/source/engine/index.ts b/source/engine/index.ts index e80a5ead4..d2883befc 100644 --- a/source/engine/index.ts +++ b/source/engine/index.ts @@ -1,24 +1,21 @@ import { evaluateControls } from 'Engine/controls' -import { ParsedRules, Rules } from 'Engine/types' +import { convertNodeToUnit, simplifyNodeUnit } from 'Engine/nodeUnits' +import { parse } from 'Engine/parse' +import { EvaluatedNode, EvaluatedRule, ParsedRules, Rules } from 'Engine/types' +import { parseUnit } from 'Engine/units' +import { mapObjIndexed } from 'ramda' import { Simulation } from 'Reducers/rootReducer' -import { evaluateNode } from './evaluation' +import { evaluationError, warning } from './error' +import { collectDefaults, evaluateNode } from './evaluation' import parseRules from './parseRules' -import { collectDefaults } from './ruleUtils' -import { parseUnit, Unit } from './units' -const emptyCache = { - _meta: { contextRule: [], defaultUnits: [] } -} - -type EngineConfig = { - rules: string | Rules | ParsedRules - useDefaultValues?: boolean -} +const emptyCache = () => ({ + _meta: { contextRule: [] } +}) type Cache = { _meta: { contextRule: Array - defaultUnits: Array inversionFail?: { given: string estimated: string @@ -26,70 +23,135 @@ type Cache = { } } +export type EvaluationOptions = Partial<{ + unit: string + useDefaultValues: boolean +}> + export { default as translateRules } from './translateRules' export { parseRules } export default class Engine { parsedRules: ParsedRules defaultValues: Simulation['situation'] situation: Simulation['situation'] = {} - cache: Cache = { ...emptyCache } + cache: Cache + cacheWithoutDefault: Cache - constructor({ rules, useDefaultValues = true }: EngineConfig) { + constructor(rules: string | Rules | ParsedRules) { + this.cache = emptyCache() + this.cacheWithoutDefault = emptyCache() this.parsedRules = typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName ? parseRules(rules) : (rules as ParsedRules) - this.defaultValues = useDefaultValues - ? collectDefaults(this.parsedRules) - : {} + + this.defaultValues = mapObjIndexed( + (value, name) => + typeof value === 'string' + ? this.evaluateExpression(value, `[valeur par défaut] ${name}`, false) + : value, + collectDefaults(this.parsedRules) + ) } private resetCache() { - this.cache = { ...emptyCache } + this.cache = emptyCache() + this.cacheWithoutDefault = emptyCache() } - setSituation(situation: Simulation['situation'] = {}) { - this.situation = situation - this.resetCache() - return this + private situationGate(useDefaultValues = true) { + return dottedName => + this.situation[dottedName] ?? + (useDefaultValues ? this.defaultValues[dottedName] : null) } - setDefaultUnits(defaultUnits: string[] = []) { - this.cache._meta.defaultUnits = defaultUnits.map(unit => - parseUnit(unit) - ) as any - return this - } - - evaluate(expression: string | Array) { - const results = (Array.isArray(expression) ? expression : [expression]).map( - expr => - this.cache[expr] || - (this.parsedRules[expr] - ? evaluateNode( - this.cache, - this.situationGate, - this.parsedRules, - this.parsedRules[expr] - ) - : // TODO: To support expressions (with operations, unit conversion, - // etc.) it should be enough to replace the above line with : - // parse(this.parsedRules, { dottedName: '' }, this.parsedRules)(expr) - // But currently there are small side effects (null values converted - // to 0), so we need to modify a little bit the engine before enabling - // publicode expressions in the UI. - - null) + private evaluateExpression( + expression: string, + context: string, + useDefaultValues: boolean = true + ): EvaluatedRule { + const result = simplifyNodeUnit( + evaluateNode( + useDefaultValues ? this.cache : this.cacheWithoutDefault, + this.situationGate(useDefaultValues), + this.parsedRules, + parse( + this.parsedRules, + { dottedName: context }, + this.parsedRules + )(expression) + ) ) - return Array.isArray(expression) ? results : results[0] + + if (Object.keys(result.defaultValue?.missingVariable ?? {}).length) { + throw new evaluationError( + context, + "Impossible d'évaluer l'expression car celle ci fait appel à des variables manquantes" + ) + } + return result + } + + setSituation( + situation: Partial> = {} + ) { + this.resetCache() + this.situation = mapObjIndexed( + (value, name) => + typeof value === 'string' + ? this.evaluateExpression(value, `[situation] ${name}`, true) + : value, + situation + ) + return this + } + + evaluate(expression: Names, options?: EvaluationOptions): EvaluatedRule + evaluate( + expression: string, + options?: EvaluationOptions + ): EvaluatedNode + evaluate( + expression: string, + options?: EvaluationOptions + ): EvaluatedNode { + let result = this.evaluateExpression( + expression, + `[evaluation] ${expression}`, + options?.useDefaultValues ?? true + ) + if (result.category === 'reference' && result.explanation) { + result = result.explanation + } + if (options?.unit) { + try { + return convertNodeToUnit( + parseUnit(options.unit), + result as EvaluatedNode + ) + } catch (e) { + warning( + `[evaluation] ${expression}`, + "L'unité demandée est incompatible avec l'expression évaluée" + ) + } + } + return result } controls() { - return evaluateControls(this.cache, this.situationGate, this.parsedRules) + return evaluateControls(this.cache, this.situationGate(), this.parsedRules) } + + inversionFail(): boolean { + return !!this.cache._meta.inversionFail + } + + getParsedRules(): ParsedRules { + return this.parsedRules + } + // TODO : this should be private getCache(): Cache { return this.cache } - situationGate = (dottedName: string) => - this.situation[dottedName] ?? this.defaultValues[dottedName] } diff --git a/source/engine/mecanismViews/Allègement.js b/source/engine/mecanismViews/Allègement.js index 2e02e1611..631c7e6e6 100644 --- a/source/engine/mecanismViews/Allègement.js +++ b/source/engine/mecanismViews/Allègement.js @@ -40,7 +40,7 @@ export default function Allègement(nodeValue, rawExplanation) { )} {explanation.plafond && ( -
  • +
  • plafond: {makeJsx(explanation.plafond)}
  • diff --git a/source/engine/mecanismViews/Barème.css b/source/engine/mecanismViews/Barème.css index a24711323..1df8d11ca 100644 --- a/source/engine/mecanismViews/Barème.css +++ b/source/engine/mecanismViews/Barème.css @@ -19,7 +19,7 @@ padding: 0.1em 0.4em; } .barème table th { - font-weight: 500; + font-weight: 600; } .barème table th:first-letter { text-transform: uppercase; diff --git a/source/engine/mecanismViews/Barème.tsx b/source/engine/mecanismViews/Barème.tsx index 25eb33fa3..d21dd4d06 100644 --- a/source/engine/mecanismViews/Barème.tsx +++ b/source/engine/mecanismViews/Barème.tsx @@ -4,8 +4,9 @@ import { Trans } from 'react-i18next' import { makeJsx } from '../evaluation' import './Barème.css' import { Node, NodeValuePointer } from './common' +import { parseUnit } from 'Engine/units' -export default function Barème(nodeValue, explanation, _, unit) { +export default function Barème(nodeValue, explanation, unit) { return (
      @@ -22,7 +23,7 @@ export default function Barème(nodeValue, explanation, _, unit) { )} diff --git a/source/engine/mecanismViews/Composantes.js b/source/engine/mecanismViews/Composantes.js index 0eb8678de..22eae557c 100644 --- a/source/engine/mecanismViews/Composantes.js +++ b/source/engine/mecanismViews/Composantes.js @@ -72,6 +72,6 @@ let Comp = function Composantes({ nodeValue, explanation, unit }) { } // eslint-disable-next-line -export default (nodeValue, explanation, _, unit) => ( +export default (nodeValue, explanation, unit) => ( ) diff --git a/source/engine/mecanismViews/Grille.tsx b/source/engine/mecanismViews/Grille.tsx index 8fa8f9038..d5192a7c2 100644 --- a/source/engine/mecanismViews/Grille.tsx +++ b/source/engine/mecanismViews/Grille.tsx @@ -3,7 +3,7 @@ import { BarèmeAttributes, TrancheTable } from './Barème' import './Barème.css' import { Node } from './common' -export default function Grille(nodeValue, explanation, _, unit) { +export default function Grille(nodeValue, explanation, unit) { return (
        diff --git a/source/engine/mecanismViews/InversionNumérique.js b/source/engine/mecanismViews/InversionNumérique.js index 797752c43..aaf3df448 100644 --- a/source/engine/mecanismViews/InversionNumérique.js +++ b/source/engine/mecanismViews/InversionNumérique.js @@ -1,49 +1,53 @@ -import { ShowValuesConsumer } from 'Components/rule/ShowValuesContext' import { makeJsx } from 'Engine/evaluation' -import { Leaf } from 'Engine/mecanismViews/common' import React from 'react' import { Node } from './common' import './InversionNumérique.css' let Comp = function InversionNumérique({ nodeValue, explanation }) { return ( - - {showValues => ( - - {!showValues || explanation.inversedWith?.value == null ? ( - <> -

        - Cette formule de calcul n'existe pas ! Mais on peut faire une - estimation à partir de : -

        -
          - {explanation.avec.map(el => ( -
        • {makeJsx(el)}
        • - ))} -
        - - ) : ( - <> - {' '} -

        - Cette valeur a été estimée à partir d'une autre variable qui - possède une formule de calcul et dont la valeur a été fixée dans - la simulation : -

        - - - )} -
        + + {explanation.inversionFailed ? ( + <> + {' '} +

        + Cette valeur devrait pouvoir être estimée à partir d'une autre + variable qui possède une formule de calcul et dont la valeur a été + fixée dans la simulation : +

        + {makeJsx(explanation.inversedWith)} +

        + Malheureusement, il a été impossible de retrouver une valeur pour + cette formule qui permette d'atterir sur la valeur demandée. +

        + + ) : explanation.inversedWith ? ( + <> + {' '} +

        + Cette valeur a été estimée à partir d'une autre variable qui possède + une formule de calcul et dont la valeur a été fixée dans la + simulation : +

        + {makeJsx(explanation.inversedWith)} + + ) : ( + <> +

        + Cette formule de calcul n'existe pas, mais on peut la calculer par + inversion en utilisant les formules des règles suivantes : +

        +
          + {explanation.inversionCandidates.map(el => ( +
        • {makeJsx(el)}
        • + ))} +
        + )} -
        + ) } diff --git a/source/engine/mecanismViews/Product.js b/source/engine/mecanismViews/Product.js index e9c5f81a1..6a1e13f70 100644 --- a/source/engine/mecanismViews/Product.js +++ b/source/engine/mecanismViews/Product.js @@ -4,7 +4,7 @@ import { Trans } from 'react-i18next' import { Node } from './common' import './InversionNumérique.css' -export default function ProductView(nodeValue, explanation, _, unit) { +export default function ProductView(nodeValue, explanation, unit) { return ( // The rate and factor and threshold are given defaut neutral values. If there is nothing to explain, don't display them at all <> - {explanation.règle && ( + {explanation.recalcul && ( - Calcul de avec : + Recalcul de la règle{' '} + avec les + valeurs suivantes : )}
          diff --git a/source/engine/mecanismViews/TauxProgressif.tsx b/source/engine/mecanismViews/TauxProgressif.tsx index f9571d272..4259c8dda 100644 --- a/source/engine/mecanismViews/TauxProgressif.tsx +++ b/source/engine/mecanismViews/TauxProgressif.tsx @@ -3,8 +3,9 @@ import { Trans } from 'react-i18next' import { BarèmeAttributes, TrancheTable } from './Barème' import './Barème.css' import { Node, NodeValuePointer } from './common' +import { Unit } from 'Engine/units' -export default function TauxProgressif(nodeValue, explanation, _, unit) { +export default function TauxProgressif(nodeValue, explanation, unit: Unit) { return ( Taux calculé :{' '} {' '} - + )}
        diff --git a/source/engine/mecanismViews/Variations.js b/source/engine/mecanismViews/Variations.js index fac9ab951..116e01f3f 100644 --- a/source/engine/mecanismViews/Variations.js +++ b/source/engine/mecanismViews/Variations.js @@ -1,5 +1,4 @@ import classnames from 'classnames' -import { ShowValuesConsumer } from 'Components/rule/ShowValuesContext' import React, { useState } from 'react' import emoji from 'react-easy-emoji' import { Trans, useTranslation } from 'react-i18next' @@ -12,109 +11,103 @@ let Comp = function Variations({ nodeValue, explanation, unit }) { let [expandedVariation, toggleVariation] = useState(null) const { i18n } = useTranslation() return ( - - {showValues => ( - - {' '} - <> -

        - - {writtenNumbers[i18n.language][explanation.length]}{' '} - - : -

        -
          - {explanation.map(({ condition, consequence, satisfied }, i) => ( -
        1. - {!satisfied && showValues && ( - <> - non applicable - {expandedVariation !== i ? ( - - ) : ( - - )} - + + {' '} + <> +

          + {writtenNumbers[i18n.language][explanation.length]} + : +

          +
            + {explanation.map(({ condition, consequence, satisfied }, i) => ( +
          1. + {!satisfied && nodeValue != null && ( + <> + non applicable + {expandedVariation !== i ? ( + + ) : ( + )} - {(expandedVariation === i || satisfied || !showValues) && ( -
            - {!condition.isDefault && ( -
            - Si : {makeJsx(condition)} -
            - )} -
            - - {!condition.isDefault ? ( - Alors - ) : ( - Sinon - )}{' '} - :  - - - {consequence && makeJsx(consequence)} - -
            + + )} + {(expandedVariation === i || satisfied || nodeValue == null) && ( +
            + {!condition.isDefault && ( +
            + Si : {makeJsx(condition)}
            )} -
          2. - ))} -
          - -
          - )} - +
          + + {!condition.isDefault ? ( + Alors + ) : ( + Sinon + )}{' '} + :  + + + {consequence && makeJsx(consequence)} + +
          +
    + )} + + ))} + + + ) } // eslint-disable-next-line -export default (nodeValue, explanation, _, unit) => ( +export default (nodeValue, explanation, unit) => ( ) diff --git a/source/engine/mecanismViews/common.tsx b/source/engine/mecanismViews/common.tsx index 397b4699f..1e5598ad7 100644 --- a/source/engine/mecanismViews/common.tsx +++ b/source/engine/mecanismViews/common.tsx @@ -1,22 +1,24 @@ import { default as classNames, default as classnames } from 'classnames' -import { SitePathsContext } from 'Components/utils/withSitePaths' -import Value, { ValueProps } from 'Components/Value' -import { ParsedRule } from 'Engine/types' +import { UseDefaultValuesContext } from 'Components/Documentation/UseDefaultValuesContext' +import { SitePathsContext } from 'Components/utils/SitePathsContext' +import { formatValue } from 'Engine/format' +import { ParsedRule, Types, Evaluation } from 'Engine/types' import { contains, isNil, pipe, sort, toPairs } from 'ramda' import React, { useContext } from 'react' import { Trans } from 'react-i18next' import { useSelector } from 'react-redux' import { Link } from 'react-router-dom' import { DottedName } from 'Rules' -import { parsedRulesSelector } from 'Selectors/analyseSelectors' import { LinkButton } from 'Ui/Button' import { capitalise0 } from '../../utils' import { encodeRuleName } from '../ruleUtils' import mecanismColors from './colors' +import { EngineContext } from 'Components/utils/EngineContext' +import { Unit } from 'Engine/units' type NodeValuePointerProps = { - data: ValueProps['nodeValue'] - unit: ValueProps['unit'] + data: Evaluation + unit: Unit } export let NodeValuePointer = ({ data, unit }: NodeValuePointerProps) => ( @@ -29,13 +31,12 @@ export let NodeValuePointer = ({ data, unit }: NodeValuePointerProps) => ( border-bottom: 0 !important; padding: 0 0.2rem; text-decoration: none !important; - font-size: 80%; box-shadow: 2px 2px 4px 1px #d9d9d9, 0 0 0 1px #d9d9d9; line-height: 1.6em; border-radius: 0.2rem; `} > - + {formatValue({ nodeValue: data, unit, language: 'fr' })} ) @@ -84,14 +85,7 @@ export function Node({
    ) ) : ( - + {(value as any) !== true && (value as any) !== false && !isNil(value) && } @@ -117,52 +111,45 @@ export function InlineMecanism({ name }: { name: string }) { } type LeafProps = { - classes: string - dottedName: DottedName - name: string + className: string + rule: ParsedRule nodeValue: NodeValuePointerProps['data'] filter: string unit: NodeValuePointerProps['unit'] } // Un élément du graphe de calcul qui a une valeur interprétée (à afficher) -export function Leaf({ - classes, - dottedName, - name, - nodeValue, - filter, - unit -}: LeafProps) { +export function Leaf({ className, rule, nodeValue, filter, unit }: LeafProps) { const sitePaths = useContext(SitePathsContext) - const rules = useSelector(parsedRulesSelector) - let rule = rules[dottedName] - const title = rule.title || capitalise0(name) + const useDefaultValues = useContext(UseDefaultValuesContext) + const title = rule.title || capitalise0(rule.name) return ( - - {dottedName && ( - - - - {rule.acronyme ? ( - {rule.acronyme} - ) : ( - title - )}{' '} - {filter} - - - {!isNil(nodeValue) && ( - - - - )} - - )} + + + + + {rule.acronyme ? {rule.acronyme} : title}{' '} + {filter} + + + {!isNil(nodeValue) && ( + + + + )} + ) } diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index ad2563cef..27f07ae73 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -1,11 +1,15 @@ import { decompose } from 'Engine/mecanisms/utils' import variations from 'Engine/mecanisms/variations' import { convertNodeToUnit } from 'Engine/nodeUnits' -import { inferUnit, isPercentUnit } from 'Engine/units' +import { + areUnitConvertible, + convertUnit, + inferUnit, + serializeUnit +} from 'Engine/units' import { any, equals, - evolve, is, map, max, @@ -13,8 +17,7 @@ import { min, path, pluck, - reduce, - toPairs + reduce } from 'ramda' import React from 'react' import { typeWarning } from './error' @@ -34,7 +37,6 @@ import InversionNumérique from './mecanismViews/InversionNumérique' import Product from './mecanismViews/Product' import Recalcul from './mecanismViews/Recalcul' import Somme from './mecanismViews/Somme' -import { disambiguateRuleReference } from './ruleUtils' import uniroot from './uniroot' import { parseUnit } from './units' @@ -132,135 +134,97 @@ export let mecanismAllOf = (recurse, k, v) => { } } -export let findInversion = (situationGate, parsedRules, v, dottedName) => { - let inversions = v.avec - if (!inversions) - throw new Error( - "Une formule d'inversion doit préciser _avec_ quoi on peut inverser la variable" - ) - /* - Quelle variable d'inversion possible a sa valeur renseignée dans la situation courante ? - Ex. s'il nous est demandé de calculer le salaire de base, est-ce qu'un candidat à l'inversion, comme - le salaire net, a été renseigné ? - */ - let candidates = inversions - .map(i => disambiguateRuleReference(parsedRules, dottedName, i)) - .map(name => { - let userInput = situationGate(name) != undefined - let rule = parsedRules[name] - if (!userInput) return null - return { - fixedObjectiveRule: rule, - userInput, - fixedObjectiveValue: situationGate(name) - } - }), - candidateWithUserInput = candidates.find(c => c && c.userInput) - - return ( - candidateWithUserInput || candidates.find(candidate => candidate != null) +let evaluateInversion = (oldCache, situationGate, parsedRules, node) => { + // TODO : take applicability into account here + let inversedWith = node.explanation.inversionCandidates.find( + n => situationGate(n.dottedName) != undefined ) -} - -let doInversion = (oldCache, situationGate, parsedRules, v, dottedName) => { - let inversion = findInversion(situationGate, parsedRules, v, dottedName) - - if (!inversion) + if (!inversedWith) { return { - missingVariables: { [dottedName]: 1 }, + ...node, + missingVariables: { + ...Object.fromEntries( + node.explanation.inversionCandidates.map(n => [n.dottedName, 1]) + ), + [node.explanation.ruleToInverse]: 1 + }, nodeValue: null } - let { fixedObjectiveValue, fixedObjectiveRule } = inversion - - let inversionCache = {} - let fx = x => { - inversionCache = { - _meta: oldCache._meta - } - let v = evaluateNode( - inversionCache, // with an empty cache - n => - dottedName === n - ? x - : n === 'sys.filter' - ? undefined - : situationGate(n), - parsedRules, - fixedObjectiveRule - ) - return v } + inversedWith = evaluateNode( + oldCache, + situationGate, + parsedRules, + inversedWith + ) + + const evaluateWithValue = n => + evaluateNode( + { + _meta: oldCache._meta + }, + dottedName => + dottedName === node.explanation.ruleToInverse + ? n + : dottedName === inversedWith.dottedName + ? undefined + : situationGate(dottedName), + parsedRules, + inversedWith + ) // si fx renvoie null pour une valeur numérique standard, disons 2000, on peut // considérer que l'inversion est impossible du fait de variables manquantes // TODO fx peut être null pour certains x, et valide pour d'autres : on peut implémenter ici le court-circuit - let attempt = fx(2000) - if (attempt.nodeValue == null) { - return attempt + const randomAttempt = evaluateWithValue(2000) + const nodeValue = + randomAttempt.nodeValue === null + ? null + : // cette fonction détermine l'inverse d'une fonction sans faire trop d'itérations + uniroot( + x => { + const candidateNode = evaluateWithValue(x) + return ( + candidateNode.nodeValue - + convertNodeToUnit(candidateNode.unit, inversedWith).nodeValue + ) + }, + node.explanation.negativeValuesAllowed ? -1000000 : 0, + 100000000, + 0.1, // tolerance + 10 // number of iteration max + ) + + if (nodeValue === undefined) { + oldCache._meta.inversionFail = true } - let tolerance = 0.1, - // cette fonction détermine l'inverse d'une fonction sans faire trop d'itérations - nodeValue = uniroot( - x => { - let y = fx(x) - return y.nodeValue - fixedObjectiveValue - }, - v['valeurs négatives possibles'] === 'oui' ? -1000000 : 0, - 10000000, - tolerance, - 10 - ) - return { - nodeValue, - missingVariables: {}, - inversionCache, - inversedWith: { - rule: fixedObjectiveRule, - value: fixedObjectiveValue - } + ...node, + nodeValue: nodeValue ?? null, + explanation: { + ...node.explanation, + inversionFail: nodeValue === undefined, + inversedWith + }, + missingVariables: randomAttempt.missingVariables } } export let mecanismInversion = dottedName => (recurse, k, v) => { - let evaluate = (cache, situationGate, parsedRules, node) => { - let inversion = - // avoid the inversion loop ! - situationGate(dottedName) == undefined && - doInversion(cache, situationGate, parsedRules, v, dottedName), - // TODO - ceci n'est pas vraiment satisfaisant - nodeValue = - situationGate(dottedName) != null - ? Number.parseFloat(situationGate(dottedName)) - : inversion.nodeValue, - missingVariables = inversion.missingVariables - if (nodeValue === undefined) { - cache._meta.inversionFail = { - given: inversion.inversedWith.rule.dottedName, - estimated: dottedName - } - } - let evaluatedNode = { - ...node, - nodeValue, - explanation: { - ...node.explanation, - inversedWith: inversion?.inversedWith - }, - missingVariables - } - // TODO - we need this so that ResultsGrid will work, but it's - // just not right - toPairs(inversion.inversionCache).map(([k, v]) => (cache[k] = v)) - return evaluatedNode + if (!v.avec) { + throw new Error( + "Une formule d'inversion doit préciser _avec_ quoi on peut inverser la variable" + ) } - return { - ...v, - evaluate, + evaluate: evaluateInversion, unit: v.unité && parseUnit(v.unité), - explanation: evolve({ avec: map(recurse) }, v), + explanation: { + ruleToInverse: dottedName, + inversionCandidates: v.avec.map(recurse), + negativeValuesAllowed: v['valeurs négatives possibles'] === 'oui' + }, jsx: InversionNumérique, category: 'mecanism', name: 'inversion numérique', @@ -269,61 +233,57 @@ export let mecanismInversion = dottedName => (recurse, k, v) => { } export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => { - let evaluate = (currentCache, situationGate, parsedRules, node) => { - let defaultRuleToEvaluate = dottedNameContext - let nodeToEvaluate = recurse(node?.règle ?? defaultRuleToEvaluate) - let cache = { _meta: { ...currentCache._meta, inRecalcul: true } } // Create an empty cache - let amendedSituation = Object.fromEntries( - Object.keys(node.avec).map(dottedName => [ - disambiguateRuleReference(parsedRules, dottedNameContext, dottedName), - node.avec[dottedName] - ]) - ) - - if (currentCache._meta.inRecalcul) { + let evaluate = (cache, situationGate, parsedRules, node) => { + if (cache._meta.inRecalcul) { return defaultNode(false) } + const recalculCache = { _meta: { ...cache._meta, inRecalcul: true } } // Create an empty cache + const amendedSituation = map( + value => evaluateNode(cache, situationGate, parsedRules, value), + node.explanation.amendedSituation + ) + const amendedSituationGate = dottedName => { + if (!(dottedName in amendedSituation)) { + return situationGate(dottedName) + } + return amendedSituation[dottedName] + } - let amendedSituationGate = dottedName => - Object.keys(amendedSituation).includes(dottedName) - ? evaluateNode( - cache, - amendedSituationGate, - parsedRules, - recurse(amendedSituation[dottedName]) - ).nodeValue - : situationGate(dottedName) - - let evaluatedNode = evaluateNode( - cache, + const evaluatedNode = evaluateNode( + recalculCache, amendedSituationGate, parsedRules, - nodeToEvaluate + node.explanation.recalcul ) return { - ...evaluatedNode, + ...node, + nodeValue: evaluatedNode.nodeValue, + ...(evaluatedNode.temporalValue && { + temporalValue: evaluatedNode.temporalValue + }), + unit: evaluatedNode.unit, explanation: { - ...evaluateNode.explanation, - unit: evaluatedNode.unit, - amendedSituation: Object.fromEntries( - Object.keys(amendedSituation).map(dottedName => [ - dottedName, - evaluateNode( - cache, - amendedSituationGate, - parsedRules, - recurse(amendedSituation[dottedName]) - ) - ]) - ) - }, - jsx: Recalcul + recalcul: evaluatedNode, + amendedSituation + } } } + const amendedSituation = Object.fromEntries( + Object.keys(v.avec).map(dottedName => [ + recurse(dottedName).dottedName, + recurse(v.avec[dottedName]) + ]) + ) + const defaultRuleToEvaluate = dottedNameContext + const nodeToEvaluate = recurse(v.règle ?? defaultRuleToEvaluate) return { - ...v, + explanation: { + recalcul: nodeToEvaluate, + amendedSituation + }, + jsx: Recalcul, evaluate } } @@ -339,7 +299,7 @@ export let mecanismSum = (recurse, k, v) => { return { evaluate, // eslint-disable-next-line - jsx: (nodeValue, explanation, _, unit) => ( + jsx: (nodeValue, explanation, unit) => ( ), explanation, @@ -371,7 +331,7 @@ export let mecanismReduction = (recurse, k, v) => { try { franchise = convertNodeToUnit(assiette.unit, franchise) plafond = convertNodeToUnit(assiette.unit, plafond) - if (!isPercentUnit(abattement.unit)) { + if (serializeUnit(abattement.unit) !== '%') { abattement = convertNodeToUnit(assiette.unit, abattement) } if (décote) { @@ -404,13 +364,13 @@ export let mecanismReduction = (recurse, k, v) => { ? montantFranchiséDécoté === 0 ? 0 : null - : isPercentUnit(abattement.unit) + : serializeUnit(abattement.unit) === '%' ? max( 0, montantFranchiséDécoté - min( plafond.nodeValue, - abattement.nodeValue * montantFranchiséDécoté + (abattement.nodeValue / 100) * montantFranchiséDécoté ) ) : max( @@ -477,10 +437,10 @@ export let mecanismProduct = (recurse, k, v) => { ) } } - let mult = (base, rate, facteur, plafond) => + const mult = (base, rate, facteur, plafond) => Math.min(base, plafond === false ? Infinity : plafond) * rate * facteur - const nodeValue = [taux, assiette, facteur].some(n => n.nodeValue === false) + let nodeValue = [taux, assiette, facteur].some(n => n.nodeValue === false) ? false : [taux, assiette, facteur].some(n => n.nodeValue === 0) ? 0 @@ -493,10 +453,14 @@ export let mecanismProduct = (recurse, k, v) => { plafond.nodeValue ) - const unit = inferUnit( + let unit = inferUnit( '*', [assiette, taux, facteur].map(el => el.unit) ) + if (areUnitConvertible(unit, assiette.unit)) { + nodeValue = convertUnit(unit, assiette.unit, nodeValue) + unit = assiette.unit + } return { nodeValue, @@ -529,6 +493,18 @@ export let mecanismProduct = (recurse, k, v) => { export let mecanismMax = (recurse, k, v) => { let explanation = v.map(recurse) + const max = (a, b) => { + if (a === false) { + return b + } + if (b === false) { + return a + } + if (a === null || b === null) { + return null + } + return Math.max(a, b) + } let evaluate = evaluateArray(max, Number.NEGATIVE_INFINITY) let jsx = (nodeValue, explanation) => ( diff --git a/source/engine/mecanisms.yaml b/source/engine/mecanisms.yaml index 8e0aec02b..c06533ad0 100644 --- a/source/engine/mecanisms.yaml +++ b/source/engine/mecanisms.yaml @@ -15,7 +15,7 @@ une de ces conditions: peut voter: formule: une de ces conditions: - - âge > 18 ans + - âge >= 18 ans - mineur émancipé toutes ces conditions: @@ -38,7 +38,7 @@ toutes ces conditions: formule: toutes ces conditions: - citoyenneté française - - âge > 18 ans + - âge >= 18 ans produit: description: >- diff --git a/source/engine/mecanisms/arrondi.tsx b/source/engine/mecanisms/arrondi.tsx index f7eb52be3..24df2d4c7 100644 --- a/source/engine/mecanisms/arrondi.tsx +++ b/source/engine/mecanisms/arrondi.tsx @@ -5,21 +5,22 @@ import { mergeAllMissing } from 'Engine/evaluation' import { Node } from 'Engine/mecanismViews/common' +import { simplifyNodeUnit } from 'Engine/nodeUnits' import { mapTemporal, pureTemporal, temporalAverage } from 'Engine/temporal' -import { EvaluatedRule } from 'Engine/types' import { serializeUnit } from 'Engine/units' +import { EvaluatedRule, Evaluation, EvaluatedNode } from 'Engine/types' import { has } from 'ramda' import React from 'react' import { Trans } from 'react-i18next' type MecanismRoundProps = { - nodeValue: EvaluatedRule['nodeValue'] + nodeValue: Evaluation explanation: ArrondiExplanation } type ArrondiExplanation = { - value: EvaluatedRule - decimals: EvaluatedRule + value: EvaluatedNode + decimals: EvaluatedNode } function MecanismRound({ nodeValue, explanation }: MecanismRoundProps) { @@ -32,16 +33,17 @@ function MecanismRound({ nodeValue, explanation }: MecanismRoundProps) { > <> {makeJsx(explanation.value)} - {explanation.decimals.isDefault !== false && ( -

    - - Arrondi à {{ count: explanation.decimals.nodeValue }} décimales - -

    - )} + {explanation.decimals.nodeValue !== false && + explanation.decimals.isDefault != false && ( +

    + + Arrondi à {{ count: explanation.decimals.nodeValue }} décimales + +

    + )} ) @@ -63,7 +65,7 @@ function evaluate( situation, parsedRules ) - const value = evaluateAttribute(node.explanation.value) + const value = simplifyNodeUnit(evaluateAttribute(node.explanation.value)) const decimals = evaluateAttribute(node.explanation.decimals) const temporalValue = mapTemporal( diff --git a/source/engine/mecanisms/durée.tsx b/source/engine/mecanisms/durée.tsx index d7de95d5f..20f29526b 100644 --- a/source/engine/mecanisms/durée.tsx +++ b/source/engine/mecanisms/durée.tsx @@ -70,7 +70,7 @@ export default (recurse, k, v) => { return { evaluate, // eslint-disable-next-line - jsx: (nodeValue, explanation, _, unit) => ( + jsx: (nodeValue, explanation, unit) => ( <> {makeJsx(explanation.valeur)} -

    +

    {!explanation.plancher.isDefault && ( )} -

    +
    ) @@ -97,7 +97,7 @@ export default (recurse, k, v) => { return { evaluate, // eslint-disable-next-line - jsx: (nodeValue, explanation, _, unit) => ( + jsx: (nodeValue, explanation, unit) => ( ', '≤', '≥'] export default (k, operatorFunction, symbol) => (recurse, k, v) => { let evaluate = (cache, situation, parsedRules, node) => { const explanation = map( @@ -54,23 +53,17 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => { let temporalValue = liftTemporal2( (a, b) => { - if (['∕', '-'].includes(node.operator) && a === false) { + if (!['≠', '='].includes(node.operator) && a === false && b === false) { return false } - if (['+'].includes(node.operator) && a === false) { - return b - } - if (['∕', '-', '×', '+'].includes(node.operator) && b === false) { - return a - } if ( - !['=', '≠'].includes(node.operator) && + ['<', '>', '≤', '≥', '∕', '×'].includes(node.operator) && (a === false || b === false) ) { return false } if ( - comparisonOperator.includes(node.operator) && + ['≠', '=', '<', '>', '≤', '≥'].includes(node.operator) && [a, b].every(value => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/)) ) { return operatorFunction(convertToDate(a), convertToDate(b)) @@ -80,7 +73,6 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => { node1.temporalValue ?? pureTemporal(node1.nodeValue), node2.temporalValue ?? pureTemporal(node2.nodeValue) ) - const nodeValue = temporalAverage(temporalValue, baseNode.unit) return { @@ -94,7 +86,7 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => { let [node1, node2] = explanation let unit = inferUnit(k, [node1.unit, node2.unit]) - let jsx = (nodeValue, explanation, _, unit) => ( + let jsx = (nodeValue, explanation, unit) => ( {(explanation[0].nodeValue !== 0 || diff --git a/source/engine/mecanisms/régularisation.ts b/source/engine/mecanisms/régularisation.ts index 5ed679cc6..d3e761d2f 100644 --- a/source/engine/mecanisms/régularisation.ts +++ b/source/engine/mecanisms/régularisation.ts @@ -1,9 +1,9 @@ import { convertToString, getYear } from 'Engine/date' import { evaluationError } from 'Engine/error' import { evaluateNode } from 'Engine/evaluation' +import { Evaluation } from 'Engine/types' import { createTemporalEvaluation, - Evaluation, groupByYear, liftTemporal2, pureTemporal, diff --git a/source/engine/mecanisms/trancheUtils.ts b/source/engine/mecanisms/trancheUtils.ts index a0032d3af..23cdf3fc7 100644 --- a/source/engine/mecanisms/trancheUtils.ts +++ b/source/engine/mecanisms/trancheUtils.ts @@ -1,5 +1,5 @@ import { mergeAllMissing } from 'Engine/evaluation' -import { Evaluation } from 'Engine/temporal' +import { Evaluation } from 'Engine/types' import { evolve } from 'ramda' import { evaluationError, typeWarning } from '../error' import { convertUnit, inferUnit } from '../units' diff --git a/source/engine/mecanisms/variations.ts b/source/engine/mecanisms/variations.ts index 213de083d..b8ddb818a 100644 --- a/source/engine/mecanisms/variations.ts +++ b/source/engine/mecanisms/variations.ts @@ -11,7 +11,6 @@ import { import { inferUnit } from 'Engine/units' import { or } from 'ramda' import { mergeAllMissing } from './../evaluation' -import { getNodeDefaultUnit } from './../nodeUnits' import { parseUnit } from './../units' /* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */ @@ -147,12 +146,7 @@ function evaluate( liftTemporal2(or, previousConditions, currentCondition) ] }, - [ - pureTemporal(false), - [], - getNodeDefaultUnit({ defaultUnit: node.unit }, cache), - pureTemporal(false) - ] + [pureTemporal(false), [], node.unit, pureTemporal(false)] ) const nodeValue = temporalAverage(temporalValue, unit) diff --git a/source/engine/nodeUnits.ts b/source/engine/nodeUnits.ts index b41e669e7..a705086c0 100644 --- a/source/engine/nodeUnits.ts +++ b/source/engine/nodeUnits.ts @@ -1,55 +1,33 @@ -import { EvaluatedNode, mapTemporal } from './temporal' -import { - areUnitConvertible, - convertUnit, - simplifyUnitWithValue, - Unit -} from './units' +import { mapTemporal } from './temporal' +import { convertUnit, simplifyUnit, Unit } from './units' +import { EvaluatedNode } from './types' export function simplifyNodeUnit(node) { - if (!node.unit || node.nodeValue === false || node.nodeValue == null) { + if (!node.unit) { return node } - const [unit, nodeValue] = simplifyUnitWithValue(node.unit, node.nodeValue) + const unit = simplifyUnit(node.unit) - 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 - ) + return convertNodeToUnit(unit, node) } -export function convertNodeToUnit(to: Unit, node: EvaluatedNode) { +export function convertNodeToUnit( + to: Unit, + node: EvaluatedNode +) { + const temporalValue = + node.temporalValue && node.unit + ? mapTemporal( + value => convertUnit(node.unit, to, value), + node.temporalValue + ) + : node.temporalValue return { ...node, nodeValue: node.unit ? convertUnit(node.unit, to, node.nodeValue) : node.nodeValue, - temporalValue: - node.temporalValue && node.unit - ? mapTemporal( - value => convertUnit(node.unit, to, value), - node.temporalValue - ) - : node.temporalValue, + ...(temporalValue && { temporalValue }), unit: to } } diff --git a/source/engine/parse.tsx b/source/engine/parse.tsx index a81d45bb8..a8604afde 100644 --- a/source/engine/parse.tsx +++ b/source/engine/parse.tsx @@ -2,7 +2,6 @@ // In a specific file // TODO import them automatically // TODO convert the legacy functions to new files -import Value from 'Components/Value' import mecanismRound, { unchainRoundMecanism } from 'Engine/mecanisms/arrondi' import barème from 'Engine/mecanisms/barème' import durée from 'Engine/mecanisms/durée' @@ -29,6 +28,7 @@ import { } from 'ramda' import React from 'react' import { EngineError, syntaxError } from './error' +import { formatValue } from './format' import grammar from './grammar.ne' import { mecanismAllOf, @@ -72,7 +72,7 @@ Utilisez leur contrepartie française : 'oui' / 'non'` const compiledGrammar = Grammar.fromCompiled(grammar) -export const parseExpression = (rule, rawNode) => { +const parseExpression = (rule, rawNode) => { /* Strings correspond to infix expressions. * Indeed, a subset of expressions like simple arithmetic operations `3 + (quantity * 2)` or like `salary [month]` are more explicit that their prefixed counterparts. * This function makes them prefixed operations. */ @@ -236,20 +236,20 @@ const statelessParseFunction = { valeur: (recurse, __, v) => recurse(v), constant: (_, __, v) => ({ type: v.type, + constant: true, nodeValue: v.nodeValue, unit: v.unit, // eslint-disable-next-line - jsx: (nodeValue, _, __, unit) => ( + jsx: (nodeValue, _, unit) => ( - + {formatValue({ + unit, + nodeValue, + language: 'fr', + // We want to display constants with full precision, + // espacilly for percentages like APEC 0,036 % + precision: 5 + })} ) }) diff --git a/source/engine/parseReference.js b/source/engine/parseReference.js index fa0f0e8e0..1a0314d9f 100644 --- a/source/engine/parseReference.js +++ b/source/engine/parseReference.js @@ -6,9 +6,9 @@ import { evaluateApplicability } from './evaluateRule' import { evaluateNode, mergeMissing } from './evaluation' import { getSituationValue } from './getSituationValue' import { Leaf } from './mecanismViews/common' -import { convertNodeToUnit, getNodeDefaultUnit } from './nodeUnits' +import { convertNodeToUnit } from './nodeUnits' import { disambiguateRuleReference } from './ruleUtils' -import { areUnitConvertible } from './units' +import { areUnitConvertible, serializeUnit } from './units' const getApplicableReplacements = ( filter, contextRuleName, @@ -56,7 +56,7 @@ const getApplicableReplacements = ( if (referenceNode.question && situationValue == null) { missingVariableList.push({ [referenceNode.dottedName]: 1 }) } - return situationValue !== false + return situationValue?.nodeValue !== false }) // Remove remplacement defined in a boolean node whose evaluated value is false .filter(({ referenceNode }) => { @@ -79,7 +79,7 @@ const getApplicableReplacements = ( : evaluateReference(filter)(cache, situation, rules, referenceNode) ) .map(replacementNode => { - const replacedRuleUnit = getNodeDefaultUnit(rule, cache) + const replacedRuleUnit = rule.unit if (!areUnitConvertible(replacementNode.unit, replacedRuleUnit)) { typeWarning( contextRuleName, @@ -149,34 +149,51 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v if (cached) return addReplacementMissingVariable(cached) - let cacheNode = (nodeValue, missingVariables, explanation, temporalValue) => { + let cacheNode = (nodeValue, missingVariables, explanation) => { cache[cacheName] = { ...node, nodeValue, - temporalValue, ...(explanation && { explanation }), + ...(explanation?.temporalValue && { + temporalValue: explanation.temporalValue + }), ...(explanation?.unit && { unit: explanation.unit }), missingVariables } return addReplacementMissingVariable(cache[cacheName]) } - const { - nodeValue: isApplicable, - missingVariables: condMissingVariables - } = evaluateApplicability(cache, situation, rules, rule) - if (!isApplicable) { - return cacheNode(isApplicable, condMissingVariables, rule) + const applicabilityEvaluation = evaluateApplicability( + cache, + situation, + rules, + rule + ) + if (!applicabilityEvaluation.nodeValue) { + return cacheNode( + applicabilityEvaluation.nodeValue, + applicabilityEvaluation.missingVariables, + applicabilityEvaluation + ) } const situationValue = getSituationValue(situation, dottedName, rule) - if (situationValue !== undefined) { - const unit = getNodeDefaultUnit(rule, cache) - return cacheNode(situationValue, condMissingVariables, { - ...rule, - nodeValue: situationValue, - unit - }) + if (situationValue != null) { + const unit = + !situationValue.unit || serializeUnit(situationValue.unit) === '' + ? rule.unit + : situationValue.unit + return cacheNode( + situationValue?.nodeValue !== undefined + ? situationValue.nodeValue + : situationValue, + applicabilityEvaluation.missingVariables, + { + ...rule, + ...(situationValue?.nodeValue !== undefined && situationValue), + unit + } + ) } if (rule.formule != null) { @@ -210,20 +227,17 @@ export let parseReference = ( parsedRules[dottedName] || // the 'inversion numérique' formula should not exist. The instructions to the evaluation should be enough to infer that an inversion is necessary (assuming it is possible, the client decides this) (!inInversionFormula && parseRule(rules, dottedName, parsedRules)) - const unit = - parsedRule.unit || parsedRule.formule?.unit || parsedRule.defaultUnit + const unit = parsedRule.unit return { evaluate: evaluateReference(filter, rule.dottedName), //eslint-disable-next-line react/display-name - jsx: (nodeValue, explanation, _, nodeUnit) => ( + jsx: (nodeValue, _, nodeUnit) => ( <> ), @@ -231,6 +245,7 @@ export let parseReference = ( category: 'reference', partialReference, dottedName, + explanation: parsedRule, unit } } diff --git a/source/engine/parseRule.tsx b/source/engine/parseRule.tsx index 7a1057375..9d1788169 100644 --- a/source/engine/parseRule.tsx +++ b/source/engine/parseRule.tsx @@ -1,4 +1,3 @@ -import { ShowValuesConsumer } from 'Components/rule/ShowValuesContext' import RuleLink from 'Components/RuleLink' import { evolve, map } from 'ramda' import React from 'react' @@ -15,7 +14,7 @@ import { nameLeaf } from './ruleUtils' import { ParsedRule, Rule, Rules } from './types' -import { parseUnit } from './units' +import { parseUnit, simplifyUnit } from './units' export default function( rules: Rules, @@ -49,32 +48,32 @@ export default function( } rawRule as Rule - const name = nameLeaf(dottedName) - let unit = rawRule.unité && parseUnit(rawRule.unité) - let defaultUnit = - rawRule['unité par défaut'] && parseUnit(rawRule['unité par défaut']) - - if (defaultUnit && unit) { - warning( - name, - 'Le paramètre `unité` est plus contraignant que `unité par défaut`.', - 'Si vous souhaitez que la valeur de votre variable soit toujours la même unité, gardez `unité`' + if ( + rawRule['par défaut'] && + rawRule['formule'] && + !rawRule.formule['une possibilité'] + ) { + throw new warning( + dottedName, + 'Une règle ne peut pas avoir à la fois une formule ET une valeur par défaut.' ) } + const name = nameLeaf(dottedName) + let unit = rawRule.unité != null ? parseUnit(rawRule.unité) : undefined + const rule = { ...rawRule, name, dottedName, type: rawRule.type, title: capitalise0(rawRule['titre'] || name), - defaultValue: rawRule['par défaut'], examples: rawRule['exemples'], icons: rawRule['icônes'], summary: rawRule['résumé'], unit, - defaultUnit, - parentDependencies + parentDependencies, + defaultValue: rawRule['par défaut'] } let parsedRule = evolve({ @@ -86,19 +85,14 @@ export default function( parents.map(parent => { let node = parse(rules, rule, parsedRules)(parent) - let jsx = (nodeValue, explanation) => ( - - {showValues => - !showValues ? ( -
    Active seulement si {makeJsx(explanation)}
    - ) : nodeValue === true ? ( -
    Active car {makeJsx(explanation)}
    - ) : nodeValue === false ? ( -
    Non active car {makeJsx(explanation)}
    - ) : null - } -
    - ) + let jsx = (nodeValue, explanation) => + nodeValue === null ? ( +
    Active seulement si {makeJsx(explanation)}
    + ) : nodeValue === true ? ( +
    Active car {makeJsx(explanation)}
    + ) : nodeValue === false ? ( +
    Non active car {makeJsx(explanation)}
    + ) : null return { evaluate: (cache, situation, parsedRules) => @@ -123,6 +117,10 @@ export default function( return disambiguateRuleReference(rules, dottedName, referenceName) }), remplace: evolveReplacement(rules, rule, parsedRules), + defaultValue: value => + typeof value === 'string' + ? parse(rules, rule, parsedRules)(value) + : value, formule: value => { let evaluate = (cache, situationGate, parsedRules, node) => { let explanation = evaluateNode( @@ -185,7 +183,10 @@ export default function( ...parsedRule, evaluate, parsed: true, - defaultUnit: parsedRule.defaultUnit || parsedRule.formule?.unit, + unit: + parsedRule.unit ?? + (parsedRule.formule?.unit && simplifyUnit(parsedRule.formule.unit)) ?? + parsedRule.defaultValue?.unit, isDisabledBy: [], replacedBy: [] } @@ -247,12 +248,12 @@ let evolveCond = (dottedName, rule, rules, parsedRules) => value => { let child = parse(rules, rule, parsedRules)(value) - let jsx = (nodeValue, explanation) => ( + let jsx = (nodeValue, explanation, unit) => ( {explanation.category === 'variable' ? (
    {makeJsx(explanation)}
    diff --git a/source/engine/react.tsx b/source/engine/react.tsx deleted file mode 100644 index db038cad3..000000000 --- a/source/engine/react.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import Value from 'Components/Value' -import React, { createContext, useContext, useMemo } from 'react' -import rules, { DottedName } from 'Rules' -import Engine from '.' -export const EngineContext = createContext<{ - engine: Engine | null - error: string | null -}>({ engine: new Engine({ rules }), error: null }) - -type InputProps = { - rules?: any - situation?: any - children: React.ReactNode -} - -export function Provider({ rules, situation, children }: InputProps) { - const [engine, error] = useMemo(() => { - try { - return [new Engine({ rules }), null] - } catch (err) { - return [null, (err?.message ?? err.toString()) as string] - } - }, [rules]) - if (engine !== null && !Object.is(situation, engine.situation)) { - engine.setSituation(situation) - } - return ( - - {children} - - ) -} - -export function useEvaluation(expression: string) { - const { engine } = useContext(EngineContext) - return engine === null ? null : engine.evaluate(expression) -} - -export function useError() { - return useContext(EngineContext).error -} - -export function Evaluation({ expression }) { - const value = useEvaluation(expression) - return value === null ? null : -} - -export default { - Provider, - useEvaluation, - useError, - Evaluation -} diff --git a/source/engine/ruleUtils.ts b/source/engine/ruleUtils.ts index 3dc507f44..5d7d19b58 100644 --- a/source/engine/ruleUtils.ts +++ b/source/engine/ruleUtils.ts @@ -1,6 +1,6 @@ import { dropLast, last, pipe, propEq, range, take } from 'ramda' import { coerceArray } from '../utils' -import { EvaluatedRule, ParsedRule, ParsedRules, Rule, Rules } from './types' +import { EvaluatedRule, Rule, Rules } from './types' export const splitName = str => str.split(' . ') export const joinName = strs => strs.join(' . ') @@ -52,22 +52,6 @@ export function disambiguateRuleReference( return dottedName } -type DefaultValues = { [name in Names]: any } | {} -export function collectDefaults( - parsedRules: ParsedRules -): DefaultValues { - return (Object.values(parsedRules) as Array>).reduce( - (acc, parsedRule) => - parsedRule?.defaultValue != null - ? { - ...acc, - [parsedRule.dottedName]: parsedRule.defaultValue - } - : acc, - {} - ) -} - /********************************* Autres */ diff --git a/source/engine/temporal.ts b/source/engine/temporal.ts index f307198e3..8fd545ebe 100644 --- a/source/engine/temporal.ts +++ b/source/engine/temporal.ts @@ -6,6 +6,7 @@ import { getRelativeDate, getYear } from 'Engine/date' +import { EvaluatedNode, Evaluation, Types } from './types' import { Unit } from './units' export type Period = { @@ -63,22 +64,12 @@ export function parsePeriod(word: string, date: Date): Period { throw new Error('Non implémenté') } -// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable) -// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)] -export type Evaluation = T | false | null - -export type EvaluatedNode = { - unit: Unit - nodeValue: Evaluation - temporalValue?: Temporal> - explanation?: Object - missingVariables?: Object -} - -export type TemporalNode = Temporal<{ nodeValue: Evaluation }> +export type TemporalNode = Temporal< + EvaluatedNode +> export type Temporal = Array & { value: T }> -export function narrowTemporalValue( +export function narrowTemporalValue( period: Period, temporalValue: Temporal> ): Temporal> { @@ -90,7 +81,7 @@ export function narrowTemporalValue( } // Returns a temporal value that's true for the given period and false otherwise. -export function createTemporalEvaluation( +export function createTemporalEvaluation( value: Evaluation, period: Period = { start: null, end: null } ): Temporal> { @@ -153,10 +144,16 @@ export function concatTemporals( ) } -export function liftTemporalNode(node: EvaluatedNode): TemporalNode { - const { temporalValue, ...baseNode } = node - if (!temporalValue) { - return pureTemporal(baseNode) +export function liftTemporalNode< + T extends Types, + Names extends string, + N extends EvaluatedNode +>(node: N): Temporal>> { + if (!('temporalValue' in node)) { + return pureTemporal(node) + } + const { temporalValue, ...baseNode } = node as N & { + temporalValue: Temporal> } return mapTemporal( nodeValue => ({ diff --git a/source/engine/types.ts b/source/engine/types.ts index 19c78b003..288f5c843 100644 --- a/source/engine/types.ts +++ b/source/engine/types.ts @@ -1,3 +1,4 @@ +import { Temporal } from './temporal' import { Unit } from './units' type Contrôle = { @@ -21,7 +22,7 @@ export type Rule = { titre?: string type?: string note?: string - suggestions?: { [description: string]: string | number } + suggestions?: { [description: string]: number } références?: { [source: string]: string } contrôles?: Array } @@ -40,7 +41,6 @@ export type ParsedRule = Rule & { API?: Object icons?: string formule?: any - suggestions?: Object evaluate?: Function explanation?: any isDisabledBy?: Array @@ -54,16 +54,36 @@ export type ParsedRules = { [name in Names]: ParsedRule } +export type Types = number | boolean | string + +// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable) +// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)] +export type Evaluation = T | false | null + +export type EvaluatedNode< + Names extends string = string, + T extends Types = Types +> = { + nodeValue: Evaluation + explanation?: Object + isDefault?: boolean + missingVariables: Partial> +} & (T extends number + ? { + unit: Unit + temporalValue?: Temporal> + } + : {}) + // This type should be defined inline by the function evaluating the rule (and // probably infered as its return type). This is only a partial definition but // it type-checks. export type EvaluatedRule< Names extends string = string, - Explanation = ParsedRule -> = ParsedRule & { - nodeValue?: number - isDefault?: boolean - isApplicable: boolean - missingVariables: Array - explanation: Explanation -} + Explanation = ParsedRule, + Type extends Types = Types +> = ParsedRule & + EvaluatedNode & { + isApplicable: boolean + explanation: Explanation + } diff --git a/source/engine/units.ts b/source/engine/units.ts index 474b67c38..a4567e01d 100644 --- a/source/engine/units.ts +++ b/source/engine/units.ts @@ -12,7 +12,7 @@ import { without } from 'ramda' import i18n from '../i18n' -import { Evaluation } from './temporal' +import { Evaluation } from './types' type BaseUnit = string @@ -163,13 +163,11 @@ function singleUnitConversionFactor( ) } function unitsConversionFactor(from: string[], to: string[]): number { - let factor = 1 - if (to.includes('%')) { - factor *= 100 - } - if (from.includes('%')) { - factor /= 100 - } + let factor = + 100 ** + // Factor is mutliplied or divided 100 for each '%' in units + (to.filter(unit => unit === '%').length - + from.filter(unit => unit === '%').length) ;[factor] = from.reduce( ([value, toUnits], fromUnit) => { const index = toUnits.findIndex( @@ -241,11 +239,19 @@ function areSameClass(a: string, b: string) { function round(value: number) { return +value.toFixed(16) } -export function simplifyUnitWithValue( - unit: Unit, - value: number = 1 -): [Unit, number] { +export function simplifyUnit(unit: Unit): Unit { + const { numerators, denominators } = simplify(unit, areSameClass) + if (numerators.length && numerators.every(symb => symb === '%')) { + return { numerators: ['%'], denominators } + } + return { + numerators: without(['%'], numerators), + denominators: without(['%'], denominators) + } +} +function simplifyUnitWithValue(unit: Unit, value: number = 1): [Unit, number] { const { denominators, numerators } = unit + const factor = unitsConversionFactor(numerators, denominators) return [ simplify( @@ -268,13 +274,13 @@ export function areUnitConvertible(a: Unit | undefined, b: Unit | undefined) { ) 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, @@ -286,13 +292,3 @@ export function areUnitConvertible(a: Unit | undefined, b: Unit | undefined) { (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 - ) -} diff --git a/source/locales/en.yaml b/source/locales/en.yaml index 8930e38fc..54bc27cfe 100644 --- a/source/locales/en.yaml +++ b/source/locales/en.yaml @@ -5,6 +5,7 @@ <0>Oui: <0>Yes A quoi servent mes cotisations ?: What's included in my contributions? Accueil: Home +Afficher la description publicode: Display publicode description Aide à la déclaration de revenus au titre de l'année 2019: Help with your 2019 income tax return Alors: Then Année d'activité: Years of activity @@ -125,7 +126,6 @@ Revenu (incluant les dépenses liées à l'activité): Revenue (including expens Revenu disponible: Disposable income 'Revenu net avec chômage partiel :': 'Net income with short-time work :' Revenu net mensuel: Monthly net income -Récapitulatif: Summary Rémunération du dirigeant: Director's remuneration Répartition du chiffre d'affaires: Breakdown of turnover Résultat: Result @@ -156,6 +156,7 @@ Vie privée: Privacy Voir la répartition des cotisations: View contribution breakdown Voir le code source: See the source code Voir les autres simulateurs: See the other simulators +Voir toutes les questions: See all questions Votre adresse e-mail: Your email address Votre entreprise: Your company Votre forme juridique: Your legal status @@ -914,7 +915,6 @@ path: gérer: déclaration-indépendant: index: /declaration-aid-independent - récapitulatif: /summary embaucher: /hiring index: /manage sécuritéSociale: /social-security diff --git a/source/locales/rules-en.yaml b/source/locales/rules-en.yaml index 57634a632..e50eb5689 100644 --- a/source/locales/rules-en.yaml +++ b/source/locales/rules-en.yaml @@ -2392,8 +2392,8 @@ contrat salarié . lodeom . réduction outre-mer: contrat salarié . lodeom . secteurs d'activité: description.en: "[automatic] To be eligible for the 1st scale of the LODEOM exemption, known as the competitiveness scale, your company must belong to one of the following sectors:\n\n- ✈ air transport providing connections between the overseas departments and regions and between metropolitan France and these territories, as well as domestic services\n- ⛵ maritime and river services or links between the French Overseas Departments and Regions\n- \U0001F3D7 building and public works\n- \U0001F4F0 the press\n- \U0001F3A5 audiovisual production\n- sectors eligible for the enhanced competitiveness (Scale 2) or innovation and growth (Scale 3) schemes, which do not meet the conditions in terms of workforce (less than 250 employees) or annual turnover (less than EUR 50 million).\n" description.fr: "Pour être éligible au 1er barème de l'exonération LODEOM, dit barème de compétitivité, votre entreprise doit appartenir à l'un des secteurs suivants :\n\n- ✈ transport aérien assurant les liaisons entre les départements et régions d’Outre-mer et entre la métropole et ces territoires, ainsi que les dessertes intérieures\n- ⛵ dessertes maritimes, fluviales ou les liaisons entre départements et régions d’Outre-mer\n- \U0001F3D7 bâtiment et travaux publics\n- \U0001F4F0 la presse\n- \U0001F3A5 la production audiovisuelle\n- les secteurs éligibles aux régimes de compétitivité renforcée (barème 2) ou d’innovation et de croissance (barème 3), qui ne respectent pas les conditions d’effectifs (moins de 250 salariés) ou de chiffres d’affaires annuel (moins de 50 millions d’euros).\n" - question.en: '[automatic] Does your company belong to one of these sectors?' - question.fr: Votre entreprise appartient-elle à l'un de ces secteurs ? + question.en: '[automatic] Does your company belong to one of the LODEOM eligible sectors?' + question.fr: Votre entreprise appartient-elle à l'un des secteurs éligible LODEOM ? titre.en: '[automatic] business areas' titre.fr: secteurs d'activité contrat salarié . lodeom . zone un: @@ -2892,9 +2892,6 @@ contrat salarié . rémunération . avantages en nature . nourriture: contrat salarié . rémunération . avantages en nature . nourriture . montant: titre.en: food titre.fr: nourriture -contrat salarié . rémunération . avantages en nature . nourriture . montant forfaitaire d'un repas: - titre.en: fixed amount of a meal - titre.fr: montant forfaitaire d'un repas contrat salarié . rémunération . avantages en nature . nourriture . repas par mois: question.en: | How many meals per month are paid for by the employer? @@ -3271,7 +3268,7 @@ contrat salarié . rémunération . revenus de remplacement: contrat salarié . rémunération . taux horaire: titre.en: '[automatic] hourly fee' titre.fr: taux horaire -contrat salarié . rémunération . taux horaire des heures supplémentaires et complémentaires: +contrat salarié . rémunération . taux horaire . heures supplémentaires: description.en: > [automatic] The hourly rate used to calculate overtime pay. It includes benefits in kind and bonuses for work performed. @@ -3279,8 +3276,8 @@ contrat salarié . rémunération . taux horaire des heures supplémentaires et Le taux horaire utilisé pour calculer la rémunération liée au heures supplémentaires. Il intègre les avantages en nature et les primes constituant la contrepartie d'un travail fourni. - titre.en: '[automatic] hourly rate for overtime and overtime' - titre.fr: taux horaire des heures supplémentaires et complémentaires + titre.en: '[automatic] hourly rate (overtime)' + titre.fr: taux horaire (heure supplémentaire) contrat salarié . rémunération . total: description.en: >- It is the gross salary, plus the employer contributions. It is the total @@ -4373,9 +4370,6 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie: à ce taux, et c'est cette implémentation que nous avons retenue. titre.en: health insurance titre.fr: maladie -dirigeant . indépendant . cotisations et contributions . cotisations . maladie . seuil supérieur de réduction: - titre.en: upper reduction threshold - titre.fr: seuil supérieur de réduction dirigeant . indépendant . cotisations et contributions . cotisations . maladie . taux: note.en: > [automatic] This contribution is basically very simple: the health @@ -5917,13 +5911,13 @@ protection sociale . retraite . complémentaire sécurité des indépendants . v protection sociale . retraite . mois cotisés: titre.en: contributed months titre.fr: mois cotisés -protection sociale . retraite . trimestres validés par an: - titre.en: quarters validated per year - titre.fr: trimestres validés par an -protection sociale . retraite . trimestres validés par an . barème trimestres générique: +protection sociale . retraite . trimestres validés: + titre.en: '[automatic] validated quarters' + titre.fr: trimestres validés +protection sociale . retraite . trimestres validés . barème trimestres générique: titre.en: generic quarters scale titre.fr: barème trimestres générique -protection sociale . retraite . trimestres validés par an . trimestres auto-entrepreneur: +protection sociale . retraite . trimestres validés . trimestres auto-entrepreneur: description.en: >- Minimum turnover thresholds for the validation of quarters for retirement as a self-employed entrepreneur. Below the minimum amount, you will only have @@ -5934,10 +5928,10 @@ protection sociale . retraite . trimestres validés par an . trimestres auto-ent n'aurez accès qu'à l'allocation de solidarité. titre.en: auto-entrepreneur quarters titre.fr: trimestres auto-entrepreneur -protection sociale . retraite . trimestres validés par an . trimestres indépendant: +protection sociale . retraite . trimestres validés . trimestres indépendant: titre.en: self-employed quarters titre.fr: trimestres indépendant -protection sociale . retraite . trimestres validés par an . trimestres salarié: +protection sociale . retraite . trimestres validés . trimestres salarié: titre.en: employee quarters titre.fr: trimestres salarié protection sociale . revenu moyen: diff --git a/source/reducers/rootReducer.ts b/source/reducers/rootReducer.ts index c6184add1..984635a08 100644 --- a/source/reducers/rootReducer.ts +++ b/source/reducers/rootReducer.ts @@ -1,13 +1,13 @@ import { Action } from 'Actions/actions' import { Unit } from 'Engine/units' -import { defaultTo, identity, omit, without } from 'ramda' +import { defaultTo, omit, without } from 'ramda' import reduceReducers from 'reduce-reducers' import { combineReducers, Reducer } from 'redux' -import originRules, { DottedName, Rules } from 'Rules' -import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' +import originRules, { Rules } from 'Rules' import { SavedSimulation } from 'Selectors/storageSelectors' import i18n, { AvailableLangs } from '../i18n' -import { areUnitConvertible, convertUnit, parseUnit } from './../engine/units' +import { DottedName } from './../rules/index' +import { objectifsSelector } from '../selectors/simulationSelectors' import inFranceAppReducer, { Company } from './inFranceAppReducer' import storageRootReducer from './storageReducer' @@ -36,15 +36,6 @@ type Example = null | { defaultUnit?: Unit } -function currentExample(state: Example = null, action: Action): Example { - switch (action.type) { - case 'SET_EXAMPLE': - return action.name != null ? action : null - default: - return state - } -} - function situationBranch(state: number | null = null, action: Action) { switch (action.type) { case 'SET_SITUATION_BRANCH': @@ -77,82 +68,22 @@ function lang( } } -function goalsFromAnalysis(analysis) { - return ( - analysis && - (Array.isArray(analysis) ? analysis[0] : analysis).targets - .map(target => target.explanation || target) - .filter(target => !!target.formule == !!target.question) - .map(({ dottedName }) => dottedName) - ) -} - -function updateSituation( - situation, - { - fieldName, - value, - analysis - }: { - fieldName: DottedName - value: any - analysis: any - } -) { - const goals = goalsFromAnalysis(analysis) - const removePreviousTarget = goals?.includes(fieldName) - ? omit(goals) - : identity - return { ...removePreviousTarget(situation), [fieldName]: value } -} - -function updateDefaultUnit(situation: Situation, { toUnit, analysis }) { - const unit = parseUnit(toUnit) - const goals = goalsFromAnalysis(analysis) - const convertedSituation = Object.keys(situation) - .map( - dottedName => - analysis.targets.find(target => target.dottedName === dottedName) || - analysis.cache[dottedName] - ) - .filter( - rule => - rule.dottedName === 'entreprise . charges' || // HACK en attendant de revoir le fonctionnement des unités - (goals?.includes(rule.dottedName) && - (rule.unit || rule.defaultUnit) && - !rule.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 = | "à l'affiche" | 'non prioritaires' | 'uniquement' | 'liste noire' -export type SimulationConfig = Partial<{ +export type SimulationConfig = { objectifs: | Array | Array<{ icône: string; nom: string; objectifs: Array }> - questions: Partial>> - bloquant: Array situation: Simulation['situation'] - branches: Array<{ nom: string; situation: SimulationConfig['situation'] }> + bloquant?: Array + questions?: Partial>> + branches?: Array<{ nom: string; situation: SimulationConfig['situation'] }> 'unité par défaut': string -}> +} type Situation = Partial> export type Simulation = { @@ -161,14 +92,14 @@ export type Simulation = { hiddenControls: Array situation: Situation initialSituation: Situation - defaultUnit: string + targetUnit: string foldedSteps: Array unfoldedStep?: DottedName | null } function getCompanySituation(company: Company): Situation { return { ...(company?.localisation && { - 'établissement . localisation': JSON.stringify(company.localisation) + 'établissement . localisation': company.localisation }), ...(company?.dateDeCréation && { 'entreprise . date de création': company.dateDeCréation.replace( @@ -182,7 +113,6 @@ function getCompanySituation(company: Company): Situation { function simulation( state: Simulation | null = null, action: Action, - analysis: any, existingCompany: Company ): Simulation | null { if (action.type === 'SET_SIMULATION') { @@ -199,7 +129,7 @@ function simulation( hiddenControls: [], situation: companySituation, initialSituation: companySituation, - defaultUnit: config['unité par défaut'] || '€/mois', + targetUnit: config['unité par défaut'] || '€/mois', foldedSteps: Object.keys(companySituation) as Array, unfoldedStep: null } @@ -220,13 +150,20 @@ function simulation( unfoldedStep: null } case 'UPDATE_SITUATION': + const targets = without( + ['entreprise . charges'], + objectifsSelector({ simulation: state }) + ) + const situation = state.situation + const { fieldName: dottedName, value } = action return { ...state, - situation: updateSituation(state.situation, { - fieldName: action.fieldName, - value: action.value, - analysis - }) + situation: { + ...(targets.includes(dottedName) + ? omit(targets, situation) + : situation), + [dottedName]: value + } } case 'STEP_ACTION': const { name, step } = action @@ -244,14 +181,10 @@ function simulation( } } return state - case 'UPDATE_DEFAULT_UNIT': + case 'UPDATE_TARGET_UNIT': return { ...state, - situation: updateDefaultUnit(state.situation, { - toUnit: action.defaultUnit, - analysis - }), - defaultUnit: action.defaultUnit + targetUnit: action.targetUnit } } return state @@ -274,18 +207,11 @@ const existingCompanyReducer = (state, action: Action) => { const mainReducer = (state, action: Action) => combineReducers({ lang, - rules, explainedVariable, // We need to access the `rules` in the simulation reducer simulation: (a: Simulation | null = null, b: Action): Simulation | null => - simulation( - a, - b, - a && analysisWithDefaultsSelector(state), - state?.inFranceApp?.existingCompany - ), + simulation(a, b, state?.inFranceApp?.existingCompany), previousSimulation: defaultTo(null) as Reducer, - currentExample, situationBranch, activeTargetInput, inFranceApp: inFranceAppReducer diff --git a/source/rules/artiste-auteur.yaml b/source/rules/artiste-auteur.yaml index 2e58cd31b..a35e8d784 100644 --- a/source/rules/artiste-auteur.yaml +++ b/source/rules/artiste-auteur.yaml @@ -6,8 +6,7 @@ artiste-auteur . revenus: artiste-auteur . revenus . traitements et salaires: titre: Revenu en traitements et salaires - unité par défaut: €/an - par défaut: 0 + par défaut: 0 €/an résumé: Le montant brut hors TVA de vos droits d'auteur (recettes précomptées) artiste-auteur . revenus . BNC: @@ -31,13 +30,11 @@ artiste-auteur . revenus . BNC . micro-bnc: artiste-auteur . revenus . BNC . recettes: titre: Revenu en BNC - unité par défaut: €/an - par défaut: 0 + par défaut: 0 €/an résumé: Le montant de vos recettes brutes hors TVA artiste-auteur . revenus . BNC . frais réels: - unité par défaut: €/an - par défaut: 0 + par défaut: 0 €/an question: Régime des frais réels BNC résumé: Montant de vos dépenses (frais professionnels, amortissements…) qui seront imputés à vos recettes afin d’établir vos bénéfices ou déficits applicable si: recettes > 0 €/an diff --git a/source/rules/conventions-collectives/hôtels-cafés-restaurants.yaml b/source/rules/conventions-collectives/hôtels-cafés-restaurants.yaml index 77f359481..9970977d5 100644 --- a/source/rules/conventions-collectives/hôtels-cafés-restaurants.yaml +++ b/source/rules/conventions-collectives/hôtels-cafés-restaurants.yaml @@ -6,7 +6,7 @@ contrat salarié . convention collective . HCR: contrat salarié . convention collective . HCR . montant forfaitaire d'un repas: remplace: - règle: rémunération . avantages en nature . nourriture . montant forfaitaire d'un repas + règle: rémunération . avantages en nature . nourriture . montant . repas forfaitaire formule: 3.62 €/repas contrat salarié . convention collective . HCR . majoration heures supplémentaires: diff --git a/source/rules/conventions-collectives/optique.yaml b/source/rules/conventions-collectives/optique.yaml index 435d4a92e..553b2480c 100644 --- a/source/rules/conventions-collectives/optique.yaml +++ b/source/rules/conventions-collectives/optique.yaml @@ -65,10 +65,9 @@ contrat salarié . convention collective . optique . salaire minimum conventionn contrat salarié . convention collective . optique . coefficient: question: Quel est le coefficient correspondant au poste du salarié ? - unité: points description: >- Se référer à la [grille fournie par la convention collective](http://opticiensreunis.org/storage/pdf/D4AciCiqHMr9mgqlTgjW0hvfPyE4w6ZxGTCihzYy.pdf#page=27). - par défaut: 110 + par défaut: 110 points contrat salarié . convention collective . optique . prévoyance: non applicable si: prévoyance obligatoire cadre diff --git a/source/rules/conventions-collectives/spectacle-vivant.yaml b/source/rules/conventions-collectives/spectacle-vivant.yaml index cd89df0bb..e4fa0f749 100644 --- a/source/rules/conventions-collectives/spectacle-vivant.yaml +++ b/source/rules/conventions-collectives/spectacle-vivant.yaml @@ -59,7 +59,7 @@ contrat salarié . intermittents du spectacle: - une de ces conditions: - convention collective . SVP question: A quel statut d'intermittent est rattaché l'employé ? - par défaut: technicien + par défaut: "'technicien'" formule: une possibilité: choix obligatoire: oui @@ -201,8 +201,7 @@ contrat salarié . intermittents du spectacle . artiste . réduction de taux . A contrat salarié . intermittents du spectacle . artiste . nombre jours travaillés: question: Pour combien de jours continus l'artiste est-il engagé ? - unité: jours - par défaut: 5 + par défaut: 5 jours contrat salarié . intermittents du spectacle . artiste . plafond proratisé: applicable si: nombre jours travaillés < 5 diff --git a/source/rules/conventions-collectives/sport.yaml b/source/rules/conventions-collectives/sport.yaml index 57eca2f83..30106263b 100644 --- a/source/rules/conventions-collectives/sport.yaml +++ b/source/rules/conventions-collectives/sport.yaml @@ -89,7 +89,7 @@ contrat salarié . convention collective . sport . cotisations . régime frais d - R1 - R2 - R3 - par défaut: R1 + par défaut: "'R1'" références: unamens.fr: https://www.umanens.fr/reglementation-couverture-sante-obligatoire/ccn-sport contrat salarié . convention collective . sport . cotisations . régime frais de santé . option . R1: @@ -206,8 +206,7 @@ contrat salarié . convention collective . sport . cotisations . assiette forfai contrat salarié . convention collective . sport . primes . nombre de manifestations: question: Combien de manifestations rémunérées le joueur a-t'il effectué ? #TODO : gérer la période - unité: manifestations - par défaut: 0 + par défaut: 0 manifestations contrat salarié . convention collective . sport . primes: titre: primes de manifestation @@ -226,8 +225,7 @@ contrat salarié . convention collective . sport . primes: contrat salarié . convention collective . sport . primes . manifestation 1: question: Quelle prime pour la première manifestation ? applicable si: nombre de manifestations > 0 - par défaut: 100 - unité: € + par défaut: 100 € contrat salarié . convention collective . sport . primes . manifestation 1 . franchise: titre: franchise manifestation 1 @@ -238,8 +236,7 @@ contrat salarié . convention collective . sport . primes . manifestation 1 . fr contrat salarié . convention collective . sport . primes . manifestation 2: question: Quelle prime pour la deuxième manifestation ? applicable si: nombre de manifestations > 1 - par défaut: 100 - unité: € + par défaut: 100 € contrat salarié . convention collective . sport . primes . manifestation 2 . franchise: titre: franchise manifestation 2 @@ -250,8 +247,7 @@ contrat salarié . convention collective . sport . primes . manifestation 2 . fr contrat salarié . convention collective . sport . primes . manifestation 3: question: Quelle prime pour la troisième manifestation ? applicable si: nombre de manifestations > 2 - par défaut: 100 - unité: € + par défaut: 100 € contrat salarié . convention collective . sport . primes . manifestation 3 . franchise: titre: franchise manifestation 3 @@ -262,8 +258,7 @@ contrat salarié . convention collective . sport . primes . manifestation 3 . fr contrat salarié . convention collective . sport . primes . manifestation 4: question: Quelle prime pour la quatrième manifestation ? applicable si: nombre de manifestations > 3 - par défaut: 100 - unité: € + par défaut: 100 € contrat salarié . convention collective . sport . primes . manifestation 4 . franchise: titre: franchise manifestation 4 @@ -274,8 +269,7 @@ contrat salarié . convention collective . sport . primes . manifestation 4 . fr contrat salarié . convention collective . sport . primes . manifestation 5: question: Quelle prime pour la cinquième manifestation ? applicable si: nombre de manifestations > 4 - par défaut: 100 - unité: € + par défaut: 100 € contrat salarié . convention collective . sport . primes . manifestation 5 . franchise: titre: franchise manifestation 5 @@ -286,8 +280,7 @@ contrat salarié . convention collective . sport . primes . manifestation 5 . fr contrat salarié . convention collective . sport . primes . autres manifestations: question: Quelles primes pour les autres manifestations ? applicable si: nombre de manifestations > 5 - par défaut: 100 - unité: € + par défaut: 100 € contrat salarié . convention collective . sport . cotisations . franchise: applicable si: entreprise . effectif < 10 diff --git a/source/rules/dirigeant.yaml b/source/rules/dirigeant.yaml index 748464ea4..5899b60ce 100644 --- a/source/rules/dirigeant.yaml +++ b/source/rules/dirigeant.yaml @@ -20,6 +20,8 @@ dirigeant . assimilé salarié: par: oui rend non applicable: - contrat salarié . convention collective + - contrat salarié . activité partielle + - contrat salarié . profession spécifique - contrat salarié . rémunération . primes - contrat salarié . rémunération . primes . fin d'année - contrat salarié . rémunération . primes . activité @@ -97,10 +99,10 @@ dirigeant . auto-entrepreneur . net de cotisations: question: Quel revenu avant impôt voulez-vous toucher ? description: Il s'agit du revenu net de cotisations et de charges, avant le paiement de l'impôt sur le revenu. formule: dirigeant . rémunération totale - cotisations et contributions - unité par défaut: €/mois + unité: €/mois dirigeant . auto-entrepreneur . cotisations et contributions: - unité par défaut: €/mois + unité: €/mois formule: somme: - cotisations @@ -157,7 +159,7 @@ dirigeant . auto-entrepreneur . cotisations et contributions . TFC . métiers: dirigeant . auto-entrepreneur . cotisations et contributions . contribution formation professionnelle: titre: Contribution à la formation professionnelle - unité par défaut: €/mois + unité: €/mois références: shine.fr: https://www.shine.fr/blog/formation-professionnelle-auto-entrepreneur/ Fiche service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23459 @@ -327,7 +329,7 @@ dirigeant . auto-entrepreneur . impôt . revenu abattu: dirigeant . auto-entrepreneur . net après impôt: titre: revenu net après impôt résumé: Avant déduction des dépenses liées à l'activité - unité par défaut: €/an + unité: €/an question: Quel est le revenu net après impôt souhaité ? description: >- Le revenu net de l'auto-entrepreneur après déduction de l'impôt @@ -355,7 +357,7 @@ dirigeant . rémunération totale: - bénéfices question: Quel montant pensez-vous dégager pour votre rémunération ? résumé: Dépensé par l'entreprise - unité par défaut: €/an + unité: €/an description: C'est ce que l'entreprise dépense en tout pour la rémunération du dirigeant. Cette rémunération "super-brute" inclut toutes les cotisations sociales à payer. @@ -427,7 +429,6 @@ dirigeant . indépendant . revenu net de cotisations: résumé: Avant déduction de l'impôt sur le revenu question: Quel revenu avant impôt voulez-vous toucher ? description: Il s'agit du revenu net de cotisations et de charges, avant le paiement de l'impôt sur le revenu. - unité par défaut: €/an références: Cerfa 2033-sd: https://www.impots.gouv.fr/portail/files/formulaires/2033-sd/2020/2033-sd_2983.pdf @@ -445,7 +446,7 @@ dirigeant . indépendant . résultat fiscal: Portail URSSAF: https://www.urssaf.fr/portail/home/independant/mes-cotisations/les-etapes-de-calcul/la-declaration-sociale-des-indep/les-revenus-pris-en-compte-pour.html dirigeant . indépendant . revenu professionnel: - unité par défaut: €/an + unité: €/an titre: revenu professionnel description: | C'est le revenu net de cotisations déductibles du travailleur indépendant, qui sert de base au calcul des cotisations pour les indépendants. @@ -493,7 +494,7 @@ dirigeant . indépendant . conjoint collaborateur . assiette: - 1/3 du Plafond de Sécurité Sociale - Option sur le revenu du chef avec partage ( ½ ou 1/3) - Option sur le revenu du chef sans partage ( ½ ou 1/3) - par défaut: forfaitaire + par défaut: "'forfaitaire'" formule: une possibilité: choix obligatoire: oui @@ -523,7 +524,7 @@ dirigeant . indépendant . conjoint collaborateur . assiette . revenu sans parta dirigeant . indépendant . conjoint collaborateur . assiette . pourcentage: question: À quelle proportion du revenu le conjoint cotise-t'il ? - par défaut: tiers + par défaut: "'tiers'" formule: une possibilité: choix obligatoire: oui @@ -644,15 +645,13 @@ dirigeant . indépendant . cotisations et contributions: - formation professionnelle - conjoint collaborateur . cotisations - (- exonérations) - unité par défaut: €/an dirigeant . indépendant . cotisations et contributions . cotisations . déduction tabac: applicable si: entreprise . catégorie d'activité . débit de tabac - unité par défaut: €/an question: Quel est le montant des revenus issus de la vente de tabac que vous souhaitez exonérer de cotisation vieillesse ? description: | Si vous exercez une activité de débit de tabac simultanément à une activité commerciale, vous avez la possibilité d’opter pour le calcul de votre cotisation d’assurance vieillesse sur le seul revenu tiré de votre activité commerciale (en effet, les remises pour débit de tabac sont soumises par ailleurs à un prélèvement vieillesse particulier). Nous attirons cependant votre attention sur le fait qu’en cotisant sur une base moins importante, excluant les revenus de débit de tabac, vos droits à retraite pour l’assurance vieillesse des commerçants en seront diminués. - par défaut: 0 + par défaut: 0 €/an dirigeant . indépendant . cotisations et contributions . cotisations . déduction tabac . revenus déduits: titre: assiette des cotisations (avec déduction tabac) @@ -704,7 +703,6 @@ dirigeant . indépendant . contrats madelin . mutuelle: dirigeant . indépendant . contrats madelin . mutuelle . montant: titre: Souscription à un contrat de mutuelle Madelin - unité par défaut: €/an question: Quel est le montant que vous versez à un contrat de mutuelle Madelin ? description: | Si vous cotisez au titre d'un contrat de mutuelle de type loi Madelin, @@ -714,10 +712,10 @@ dirigeant . indépendant . contrats madelin . mutuelle . montant: Fiche impôts: https://www.impots.gouv.fr/portail/particulier/questions/je-cotise-un-contrat-madelin-quel-est-mon-avantage-fiscal Bofip (contrats d'assurance de groupe): https://bofip.impots.gouv.fr/bofip/4639-PGP.html Article de loi: https://www.legifrance.gouv.fr/affichCodeArticle.do?idArticle=LEGIARTI000029042287&cidTexte=LEGITEXT000006069577&dateTexte=20140530&fastReqId=1900907951&nbResultRech=1 - par défaut: 0 + par défaut: 0 €/an dirigeant . indépendant . contrats madelin . mutuelle . plafond: - unité par défaut: €/an + unité: €/an formule: somme: - produit: @@ -739,7 +737,6 @@ dirigeant . indépendant . contrats madelin . retraite: dirigeant . indépendant . contrats madelin . retraite . montant: titre: Souscription à une retraite Madelin - unité par défaut: €/an question: Quel est le montant que vous versez à votre contrat Madelin retraite ? description: | Si vous cotisez au titre d'un contrat retraite de type loi Madelin, @@ -749,10 +746,10 @@ dirigeant . indépendant . contrats madelin . retraite . montant: Fiche impôts: https://www.impots.gouv.fr/portail/particulier/questions/je-cotise-un-contrat-madelin-quel-est-mon-avantage-fiscal Bofip (contrats d'assurance de groupe): https://bofip.impots.gouv.fr/bofip/4639-PGP.html Article de loi: https://www.legifrance.gouv.fr/affichCodeArticle.do?idArticle=LEGIARTI000029042287&cidTexte=LEGITEXT000006069577&dateTexte=20140530&fastReqId=1900907951&nbResultRech=1 - par défaut: 0 + par défaut: 0 €/an dirigeant . indépendant . contrats madelin . retraite . plafond: - unité par défaut: €/an + unité: €/an formule: le maximum de: - barème: @@ -782,11 +779,11 @@ dirigeant . rattachement CIPAV: - contrat salarié = non - entreprise . catégorie d'activité . libérale règlementée - toutes ces conditions: - - dirigeant = 'indépendant' + - dirigeant . indépendant - entreprise . date de création < 01/2019 - entreprise . catégorie d'activité = 'libérale' - toutes ces conditions: - - dirigeant = 'auto-entrepreneur' + - dirigeant . auto-entrepreneur - entreprise . date de création < 01/2018 - entreprise . catégorie d'activité = 'libérale' @@ -867,23 +864,18 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie dirigeant . indépendant . cotisations et contributions . cotisations . maladie . taux RSA: formule: - produit: - assiette: taux RSA part variable + 1.35% + encadrement: + valeur: taux RSA part variable + 1.35% plafond: 6.35% note: | Pour les indépendants au RSA, seule la réduction simple définie dans le décret de calcul de la cotisation maladie est prise en compte. La réduction renforcée en-dessous de 40% du plafond de la sécurité sociale ne l'est pas, car il n'y a pas d'assiette minimale. dirigeant . indépendant . cotisations et contributions . cotisations . maladie . taux RSA part variable: + unité: '%' formule: produit: assiette: 5% - taux: assiette des cotisations / seuil supérieur de réduction - -dirigeant . indépendant . cotisations et contributions . cotisations . maladie . seuil supérieur de réduction: - formule: - produit: - assiette: plafond sécurité sociale temps plein - taux: 110% + taux: assiette des cotisations / (110% * plafond sécurité sociale temps plein) dirigeant . indépendant . cotisations et contributions . cotisations . maladie . taux: formule: @@ -968,7 +960,7 @@ dirigeant . rattachement CIPAV . invalidité et décès . classe de cotisation: - A - B - C - par défaut: A + par défaut: "'A'" dirigeant . rattachement CIPAV . invalidité et décès . classe de cotisation . A: titre: classe A @@ -1092,10 +1084,9 @@ dirigeant . indépendant . revenus étrangers: par défaut: non dirigeant . indépendant . revenus étrangers . montant: - unité par défaut: €/an titre: revenus perçu à l'étranger question: Quel est leur montant ? - par défaut: 0 + par défaut: 0 €/an dirigeant . indépendant . cotisations et contributions . CSG et CRDS . assiette: note: >- @@ -1261,8 +1252,7 @@ dirigeant . indépendant . IJSS . total: maternel, l'indemnité journalière forfaitaire d'interruption d'activité, l'indemnité de remplacement pour maternité, paternité ou adoption et l'indemnité journalière maladie. - par défaut: 0 - unité: €/an + par défaut: 0 €/an dirigeant . indépendant . IJSS . imposable: titre: indemnités journalières imposable @@ -1287,5 +1277,4 @@ dirigeant . indépendant . IJSS . imposable: maternel, l'indemnité journalière forfaitaire d'interruption d'activité, l’indemnité de remplacement pour maternité, paternité ou adoption et l'indemnité journalière maladie. - par défaut: 0 - unité: €/an + par défaut: 0 €/an diff --git a/source/rules/déclaration-revenu-indépendant.yaml b/source/rules/déclaration-revenu-indépendant.yaml index f5259b1d8..e38fc2779 100644 --- a/source/rules/déclaration-revenu-indépendant.yaml +++ b/source/rules/déclaration-revenu-indépendant.yaml @@ -9,7 +9,7 @@ aide déclaration revenu indépendant 2019: aide déclaration revenu indépendant 2019 . nature de l'activité: remplace: entreprise . catégorie d'activité question: Quelle est la nature de votre activité ? - par défaut: commerciale ou industrielle + par défaut: "'commerciale ou industrielle'" formule: une possibilité: choix obligatoire: oui @@ -71,7 +71,7 @@ aide déclaration revenu indépendant 2019 . SMIC 2019: aide déclaration revenu indépendant 2019 . revenu net fiscal: titre: revenu net fiscal résumé: avant déduction des charges sociales et exonérations fiscales [A] - unité par défaut: €/an + unité: €/an formule: dirigeant . rémunération totale aide déclaration revenu indépendant 2019 . CSG déductible: diff --git a/source/rules/entreprise-établissement.yaml b/source/rules/entreprise-établissement.yaml index b5a5e9b98..83f19a7d2 100644 --- a/source/rules/entreprise-établissement.yaml +++ b/source/rules/entreprise-établissement.yaml @@ -50,13 +50,13 @@ entreprise . chiffre d'affaires: titre: chiffre d'affaires (H.T.) question: Quel est votre chiffre d'affaires envisagé ? résumé: Le montant des ventes réalisées - unité par défaut: €/an + unité: €/an formule: dirigeant . rémunération totale + charges entreprise . chiffre d'affaires minimum: description: Le montant minimum des ventes (H.T) à réaliser pour atteindre le seuil de rentabilité. question: Quel est votre chiffre d'affaires minimum envisagé ? - unité par défaut: €/an + unité: €/an formule: chiffre d'affaires entreprise . chiffre d'affaires de société: @@ -69,8 +69,7 @@ entreprise . rémunération du dirigeant: description: | C'est la part du chiffre d'affaires après charges qui est allouée à la rémunération du dirigeant. Plus cette part est élevée, plus la rémunération du dirigeant augmente, et plus le bénéfice de l'entreprise diminue. question: Quelle part du chiffre d'affaires après charge est allouée à la rémunération du dirigeant ? - unité: '%' - par défaut: 1 + par défaut: 100% entreprise . bénéfice: formule: chiffre d'affaires - charges dont rémunération dirigeant @@ -122,8 +121,7 @@ entreprise . charges: références: Charges déductibles ou non du résultat fiscal d'une entreprise: https://www.service-public.fr/professionnels-entreprises/vosdroits/F31973 - par défaut: 0 - unité par défaut: €/an + par défaut: 0 €/an entreprise . ACRE: description: >- @@ -159,7 +157,7 @@ entreprise . ACRE: applicable si: une de ces conditions: - toutes ces conditions: - - dirigeant = 'auto-entrepreneur' + - dirigeant . auto-entrepreneur - entreprise . durée d'activité < 3 ans - entreprise . date de création < 01/01/2020 - entreprise . durée d'activité . en début d'année < 1 an @@ -233,7 +231,7 @@ entreprise . effectif . seuil: - moins de 50 - moins de 250 - 251 et plus - par défaut: moins de 5 + par défaut: "'moins de 5'" entreprise . effectif . seuil . moins de 5: entreprise . effectif . seuil . moins de 11: @@ -247,11 +245,10 @@ entreprise . ratio alternants: titre: Fraction d'alternants description: | Cette fraction détermine la contribution supplémentaire pour l'apprentissage pour les entreprises concernées. - unité: '%' suggestions: 1%: 0.01 5%: 0.05 - par défaut: 0 + par défaut: 0% entreprise . association non lucrative: description: L'entreprise est une association non lucrative @@ -289,7 +286,7 @@ entreprise . catégorie d'activité: titre: nature de l'activité question: Quelle est la nature de votre activité ? description: Votre catégorie d'activité va déterminer une grande partie des calculs de cotisation, contribution et impôt. - par défaut: commerciale ou industrielle + par défaut: "'commerciale ou industrielle'" formule: une possibilité: choix obligatoire: oui @@ -346,7 +343,7 @@ entreprise . catégorie d'activité . service ou vente: possibilités: - service - vente - par défaut: vente + par défaut: "'vente'" entreprise . catégorie d'activité . service ou vente . vente: titre: vente de biens diff --git a/source/rules/impôt.yaml b/source/rules/impôt.yaml index ac8c89c6c..72fba15f5 100644 --- a/source/rules/impôt.yaml +++ b/source/rules/impôt.yaml @@ -31,7 +31,7 @@ impôt . méthode de calcul: # applicable si: revenu imposable > 0 # bizarrement, cette condition ne semble pas marcher, on se résout donc à utiliser une version plus "hacky" et moins proche de la loi. Elle posera problème le jour où l'on aura a calculer l'impot avec plusieurs sources de revenu non applicable si: dirigeant . auto-entrepreneur . impôt . versement libératoire - par défaut: barème standard + par défaut: "'barème standard'" formule: une possibilité: choix obligatoire: oui @@ -256,7 +256,7 @@ impôt . taux personnalisé: revenus net de cotisations: résumé: Avant impôt - unité par défaut: €/an + unité: €/an question: Quel revenu avant impôt voulez-vous toucher ? description: | Il s'agit du revenu net de cotisations et de charges, avant le paiement de l'impôt sur le revenu. @@ -267,7 +267,7 @@ revenus net de cotisations: - dirigeant . auto-entrepreneur . net de cotisations revenu net après impôt: - unité par défaut: €/an + unité: €/an résumé: Disponible sur votre compte en banque question: Quel revenu voulez-vous toucher ? description: | diff --git a/source/rules/protection-sociale.yaml b/source/rules/protection-sociale.yaml index 3cda86c32..3b4ff4742 100644 --- a/source/rules/protection-sociale.yaml +++ b/source/rules/protection-sociale.yaml @@ -60,14 +60,14 @@ protection sociale . retraite . base . taux de la pension: description: Le taux appliqué, avec décote ou surcote en fonction du nombre de trimestres cotisés. formule: variations: - - si: trimestres validés par an = 0 + - si: trimestres validés = 0 alors: 0% - sinon: 50% note: On ne prends pas en compte la décote du taux suite aux trimestres manquant. On considère donc que le cotisant part à taux plein, donc à 67 ans (ou avant si tous les trimestres sont validés). références: service-public.fr: https://www.service-public.fr/particuliers/vosdroits/F19666 -protection sociale . retraite . trimestres validés par an: +protection sociale . retraite . trimestres validés: unité: trimestres validés/an formule: somme: @@ -76,14 +76,14 @@ protection sociale . retraite . trimestres validés par an: - trimestres auto-entrepreneur plafond: 4 -protection sociale . retraite . trimestres validés par an . trimestres salarié: +protection sociale . retraite . trimestres validés . trimestres salarié: unité: trimestres validés/an applicable si: contrat salarié formule: barème trimestres générique -protection sociale . retraite . trimestres validés par an . trimestres indépendant: +protection sociale . retraite . trimestres validés . trimestres indépendant: unité: trimestres validés/an - applicable si: dirigeant = 'indépendant' + applicable si: dirigeant . indépendant formule: variations: - si: situation personnelle . RSA @@ -92,7 +92,7 @@ protection sociale . retraite . trimestres validés par an . trimestres indépen valeur: barème trimestres générique plancher: 3 -protection sociale . retraite . trimestres validés par an . barème trimestres générique: +protection sociale . retraite . trimestres validés . barème trimestres générique: unité: trimestres validés/an formule: grille: @@ -112,8 +112,8 @@ protection sociale . retraite . trimestres validés par an . barème trimestres références: cnav.fr: https://www.legislation.cnav.fr/Pages/bareme.aspx?Nom=salaire_validant_un_trimestre_montant_bar -protection sociale . retraite . trimestres validés par an . trimestres auto-entrepreneur: - applicable si: dirigeant = 'auto-entrepreneur' +protection sociale . retraite . trimestres validés . trimestres auto-entrepreneur: + applicable si: dirigeant . auto-entrepreneur description: Les seuils de chiffre d'affaires minimum pour la validation des trimestres pour la retraite en auto-entrepreneur. En-dessous du montant minimum, vous n'aurez accès qu'à l'allocation de solidarité. unité: trimestres validés/an formule: @@ -276,7 +276,7 @@ protection sociale . santé . indemnités journalières: - indemnités journalières . salarié protection sociale . santé . indemnités journalières . auto-entrepreneur: - applicable si: dirigeant = 'auto-entrepreneur' + applicable si: dirigeant . auto-entrepreneur unité: €/jour formule: @@ -292,7 +292,7 @@ protection sociale . santé . indemnités journalières . auto-entrepreneur: - secu-independants.fr: https://www.secu-independants.fr/sante/indemnites-journalieres/montant-de-lindemnite protection sociale . santé . indemnités journalières . indépendant: - applicable si: dirigeant = 'indépendant' + applicable si: dirigeant . indépendant unité: €/jour formule: diff --git a/source/rules/salarié.yaml b/source/rules/salarié.yaml index 7e0b21442..5601b8e45 100644 --- a/source/rules/salarié.yaml +++ b/source/rules/salarié.yaml @@ -9,7 +9,7 @@ contrat salarié: - CDD - apprentissage - stage - par défaut: CDI + par défaut: "'CDI'" description: | Le contrat qui lie une entreprise (via son établissement) à un individu, qui est alors son salarié. @@ -130,8 +130,7 @@ contrat salarié . frais professionnels . titres-restaurant . part déductible: contrat salarié . frais professionnels . titres-restaurant . titres-restaurant par mois: question: Combien de titres-restaurants sont distribués au salarié ? - par défaut: 19 - unité: titres-restaurant/mois + par défaut: 19 titres-restaurant/mois contrat salarié . frais professionnels . titres-restaurant . montant unitaire: question: Quelle est la valeur unitaire du titre-restaurant ? @@ -139,20 +138,18 @@ contrat salarié . frais professionnels . titres-restaurant . montant unitaire: Il n'y a pas de valeur maximale ou minimale pour les titres-restaurant. En revanche, pour bénéficier de l'exonération de cotisation, il ne faut pas dépasser 11,10€ par titre en 2020. - par défaut: 8 + par défaut: 8 €/titre-restaurant suggestions: faible: 6 moyenne: 8 max exonéré: 11.10 - unité: €/titre-restaurant contrat salarié . frais professionnels . titres-restaurant . taux participation employeur: description: >- Part du titre-restaurant payée par l'employeur. Doit être de 50% minimum et de 60% maximum. question: Quelle est la participation de l'employeur ? - par défaut: 50 - unité: '%' + par défaut: 50 % suggestions: 50%: 50 60%: 60 @@ -194,14 +191,13 @@ contrat salarié . frais professionnels . indemnité kilométrique vélo . part plafond: 200 €/an contrat salarié . frais professionnels . indemnité kilométrique vélo . distance mensuelle: - unité: km/mois question: >- Quelle est la distance parcourue en vélo chaque mois pour le trajet domicile / travail ? suggestions: 2 km/jour: 40 5 km/jour: 100 10 km/jour: 200 - par défaut: 80 + par défaut: 80 km/mois contrat salarié . activité partielle: question: Le salarié est-il en chômage partiel ? @@ -248,8 +244,7 @@ contrat salarié . activité partielle . heures travaillées: description: >- Dans le cadre du chômage partiel, le nombre d'heure restante travaillées. Doit être inférieur au temps contractuel. - unité: heures/mois - par défaut: 0 + par défaut: 0 heures/mois suggestions: 30 h/semaine: 130 20 h/semaine: 86.6666 @@ -338,12 +333,13 @@ contrat salarié . activité partielle . indemnisation entreprise: Dans le cadre de la crise du Coronavirus, le gouvernement a anoncé que l'indemnité de chômage partiel sera prise à 100% en charge par l'état. formule: - valeur: indemnités . base - plancher: 8.03 €/heure * heures chômées - plafond: - recalcul: - avec: - rémunération . brut de base: 4.5 * SMIC + encadrement: + valeur: indemnités . base + plancher: 8.03 €/heure * heures chômées + plafond: + recalcul: + avec: + rémunération . brut de base: 4.5 * SMIC # TODO : This should be merged with other convention collectives contrat salarié . activité partielle . convention syntec: @@ -450,28 +446,28 @@ contrat salarié . CDD . CPF: situation: CDD: non cotisations . assiette: 1480 - valeur attendue: 0 + valeur attendue: false - nom: SMIC situation: CDD: oui - événement: aucun - motif: accroissement activité + événement: non + motif: "'accroissement activité'" contrat jeune vacances: non cotisations . assiette: 1480 valeur attendue: 14.8 - nom: salaire médian situation: CDD: oui - événement: aucun - motif: accroissement activité + événement: non + motif: "'accroissement activité'" contrat jeune vacances: non cotisations . assiette: 2300 valeur attendue: 23 - nom: motif saisonnier -> non applicable situation: - contrat salarié . CDD . motif: classique . saisonnier + contrat salarié . CDD . motif: "'classique . saisonnier'" cotisations . assiette: 2300 - valeur attendue: 0 + valeur attendue: false contrat salarié . CDD . compensation pour congés non pris: titre: indemnité de congés payés @@ -519,26 +515,26 @@ contrat salarié . CDD . compensation pour congés non pris: exemples: - nom: pas de congés non pris situation: - rémunération . brut de base: 2300 + rémunération . brut de base: 2300 €/mois prime de fin de contrat: 0 congés non pris: 0 durée contrat: 12 - valeur attendue: 0 + valeur attendue: false - nom: 10 jours non pris situation: CDD: oui - rémunération . brut de base: 2300 + rémunération . brut de base: 2300 €/mois prime de fin de contrat: 0 - congés non pris: 10 - durée contrat: 12 + congés non pris: 10 jours ouvrés + durée contrat: 12 mois valeur attendue: 92 - nom: 3 jours non pris situation: CDD: oui - rémunération . brut de base: 2300 + rémunération . brut de base: 2300 €/mois prime de fin de contrat: 0 - congés non pris: 3 - durée contrat: 6 + congés non pris: 3 jours ouvrés + durée contrat: 6 mois valeur attendue: 55.21 note: | L'indemnité est versée à la fin du contrat, sauf si le CDD se poursuit par un CDI. @@ -613,14 +609,14 @@ contrat salarié . CDD . prime de fin de contrat: situation: CDD: oui rémunération . brut de base: 2300 - motif: classique . accroissement activité + motif: "'classique . accroissement activité'" valeur attendue: 230 - nom: CDD d'usage -> non applicable situation: - motif: classique . usage + motif: "'classique . usage'" rémunération . brut de base: 2300 - valeur attendue: 0 + valeur attendue: false références: Code du travail - Article L1243-8: https://www.legifrance.gouv.fr/affichCode.do?idSectionTA=LEGISCTA000006189459&cidTexte=LEGITEXT000006072050 @@ -674,8 +670,7 @@ contrat salarié . ATMP . taux collectif ATMP: Les entreprises de moins de 20 salariés sont assujetties à ce taux collectif. Pour les entreprises plus importantes, ce taux est modulé (jusqu'à 150 salariés) voire individualisé (au-delà). L'entreprise peut consulter le taux qui la concerne en ligne sur [net-entreprise](http://www.net-entreprises.fr/html/compte-accident-travail.htm). - par défaut: 2.22 - unité par défaut: '%' + par défaut: 2.22 % references: taux moyen national: https://www.legifrance.gouv.fr/affichTexteArticle.do;jsessionid=4702534627E4A8CF240B990E28C81AF4.tplgfr30s_3?idArticle=JORFARTI000033735834&cidTexte=JORFTEXT000033735824&dateTexte=29990101&categorieLien=id @@ -693,11 +688,12 @@ contrat salarié . CDD . événement: # elle apparaîtrait alors forcément _après_ la question du motif formule: une possibilité: - - poursuite du CDD en CDI - - refus CDI avantageux - - rupture anticipée salarié - - rupture pour faute grave ou force majeure - - rupture pendant période essai + possibilités: + - poursuite du CDD en CDI + - refus CDI avantageux + - rupture anticipée salarié + - rupture pour faute grave ou force majeure + - rupture pendant période essai par défaut: non contrat salarié . CDD . événement . poursuite du CDD en CDI: @@ -747,7 +743,7 @@ contrat salarié . CDD . motif: - complément formation - issue d'apprentissage # les CDD d'usage "concentrent la moitié des embauches en CDD" - par défaut: classique . usage + par défaut: "'classique . usage'" références: Code du travail - Articles L1242-1 à 4: https://www.legifrance.gouv.fr/affichCode.do;jsessionid=E318966AA9DEB9E32465297F15B04D86.tpdila20v_1?idSectionTA=LEGISCTA000006195639&cidTexte=LEGITEXT000006072050&dateTexte=20170420 le recours au CDD: http://www.entreprises.cci-paris-idf.fr/web/reglementation/developpement-entreprise/droit-social/le-recours-au-cdd @@ -758,14 +754,15 @@ contrat salarié . CDD . motif . classique: titre: motifs classiques formule: une possibilité: - - remplacement - - accroissement activité - - saisonnier - - usage - - mission + possibilités: + - remplacement + - accroissement activité + - saisonnier + - usage + - mission références: Code du travail - Article L1242-2: https://www.legifrance.gouv.fr/affichCodeArticle.do;jsessionid=714D2E2B814371F4F1D5AA88472CD621.tpdila20v_1?idArticle=LEGIARTI000033024658&cidTexte=LEGITEXT000006072050&dateTexte=20170420 - par défaut: usage + par défaut: "'usage'" contrat salarié . CDD . motif . classique . saisonnier: titre: Saisonnier @@ -870,25 +867,23 @@ contrat salarié . CDD . durée contrat: [Cliquez ici](https://www.service-public.fr/professionnels-entreprises/vosdroits/F31211) pour connaître la durée maximale d'un CDD. références: Durée maximale d'un CDD (service-public.fr): https://www.service-public.fr/professionnels-entreprises/vosdroits/F31211 - unité: mois suggestions: 18 mois: 18 1 an: 12 6 mois: 6 3 mois: 3 # 70% des contrats signés ont concerné, en 2015, des durées inférieures à un mois - par défaut: 1 + par défaut: 1 mois contrat salarié . CDD . congés non pris: question: Combien de jours ouvrés de congés ne seront pas pris sur la durée du CDD ? description: | Le contrat étant à durée déterminée, le salarié n'a pas forcément le temps de prendre tous les jours de congés qu'il a acquis comme tout salarié au cours du contrat. Par exemple, pour un contrat de 3 mois, le salarié acquiert 2,08 jours de congés par mois (25 jours / 12 mois = 2,08), donc 6,25 sur la durée du contrat. Or il se peut que l'entreprise le contraigne à n'en prendre que 4, donc 2,25 jours ne seront pas pris. Ils seront payés par l'employeur à la fin du contrat. - unité: jour ouvré suggestions: 3: 3 10: 10 - par défaut: 0 + par défaut: 0 jour ouvré contrôles: - si: congés non pris > congés dus en jours ouvrés message: Un salarié acquiert normalement 2.08 jours de congés ouvrés par mois. @@ -930,7 +925,7 @@ contrat salarié . apprentissage . diplôme préparé: possibilités: - niveau bac ou moins - niveau supérieur au bac - par défaut: niveau supérieur au bac + par défaut: "'niveau supérieur au bac'" contrat salarié . apprentissage . diplôme préparé . niveau bac ou moins: titre: Diplôme d'un niveau inférieur ou égal au bac @@ -952,7 +947,7 @@ contrat salarié . apprentissage . ancienneté: - moins de deux ans - moins de trois ans - moins de quatre ans - par défaut: moins d'un an + par défaut: "'moins d'un an'" contrôles: - si: moins de quatre ans niveau: information @@ -1030,7 +1025,6 @@ contrat salarié . cotisations . assiette: description: | L'assiette des cotisations sociales est la base de calcul d'un grand nombre de cotisations sur le travail salarié. Elle comprend notamment les rémunérations en espèces (salaire de base, indemnité, primes...) et les avantages en nature (logement, véhicule...). référence: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/la-base-de-calcul.html - unité par défaut: €/mois formule: allègement: assiette: rémunération . brut @@ -1072,7 +1066,7 @@ contrat salarié . rémunération . brut de base: C'est le salaire *brut* régulier inscrit dans le contrat de travail. Il ne change jamais entre les mois et ne peut pas être modifié sans signature des deux parties. Il ne comprend pas les indemnités, avantages sociaux, avantages en nature et primes... - unité par défaut: €/mois + unité: €/mois suggestions: salaire médian: 2300 SMIC: 1539 @@ -1112,9 +1106,9 @@ contrat salarié . rémunération . brut de base: - rémunération . net - rémunération . net après impôt - équivalent temps plein + - dirigeant . rémunération totale - entreprise . chiffre d'affaires - entreprise . chiffre d'affaires minimum - - dirigeant . rémunération totale références: Le salaire. Fixation et paiement: http://travail-emploi.gouv.fr/droit-du-travail/remuneration-et-participation-financiere/remuneration/article/le-salaire-fixation-et-paiement @@ -1124,7 +1118,7 @@ contrat salarié . rémunération . brut de base . équivalent temps plein: titre: Salaire brut équivalent temps plein résumé: Le salaire si l'embauche se faisait à temps plein question: Quel est le salaire en équivalent temps plein ? - unité par défaut: €/mois + unité: €/mois formule: brut de base / temps de travail . quotité de travail suggestions: salaire médian: 2300 @@ -1134,7 +1128,8 @@ contrat salarié . rémunération . taux horaire: unité: €/heure formule: assiette de vérification du SMIC / temps de travail -contrat salarié . rémunération . taux horaire des heures supplémentaires et complémentaires: +contrat salarié . rémunération . taux horaire . heures supplémentaires: + titre: taux horaire (heure supplémentaire) description: > Le taux horaire utilisé pour calculer la rémunération liée au heures supplémentaires. Il intègre les avantages en nature et les primes @@ -1191,7 +1186,6 @@ contrat salarié . rémunération . primes: Sauf exception, elles sont soumises aux cotisations sociales et à l'impôt sur le revenu. - unité par défaut: €/mois formule: somme: @@ -1218,9 +1212,8 @@ contrat salarié . rémunération . primes . activité: contrat salarié . rémunération . primes . activité . base: titre: primes d'activité - unité: €/mois question: Quel est le montant des primes liées à l'activité du salarié ? - par défaut: 0 + par défaut: 0 €/mois contrat salarié . rémunération . primes . activité . conventionnelles: formule: 0 €/mois @@ -1260,7 +1253,6 @@ contrat salarié . rémunération . primes . fin d'année . treizième mois: contrat salarié . rémunération . brut: description: Toutes les sommes versées au salarié sous forme monétaire en échange de son travail. titre: Rémunération brute - unité par défaut: €/mois formule: # Pour les frais professionnels, et les cotisations de prévoyance facultative # et de retraite complémentaire on ne ré-intègre ici que la part employeur @@ -1283,11 +1275,9 @@ contrat salarié . rémunération . brut: contrat salarié . rémunération . heures supplémentaires: titre: rémunération heures supplémentaires description: La rémunération relative aux heures supplémentaires - unité par défaut: €/mois - formule: produit: - assiette: taux horaire des heures supplémentaires et complémentaires + assiette: taux horaire . heures supplémentaires facteur: somme: - temps de travail . heures supplémentaires @@ -1296,11 +1286,9 @@ contrat salarié . rémunération . heures supplémentaires: contrat salarié . rémunération . heures complémentaires: titre: rémunération heures complémentaires description: La rémunération relative aux heures complémentaires - unité par défaut: €/mois - formule: produit: - assiette: taux horaire des heures supplémentaires et complémentaires + assiette: taux horaire . heures supplémentaires facteur: somme: - temps de travail . heures complémentaires @@ -1379,10 +1367,9 @@ contrat salarié . rémunération . avantages en nature . autres . montant: titre: autres question: > Quel est le montant de ces autres avantages ? - par défaut: 0 + par défaut: 0 €/mois suggestions: 🚗 véhicule: 260 - unité par défaut: €/mois contrat salarié . rémunération . avantages en nature . ntic . montant: titre: outils NTIC @@ -1407,9 +1394,7 @@ contrat salarié . rémunération . avantages en nature . ntic . montant: contrat salarié . rémunération . avantages en nature . ntic . coût appareils: question: > Quel est le coût total neuf des appareils mis à disposition ? - unité: € - - par défaut: 800 + par défaut: 800 € # TODO : vérifier et documenter les chiffres suggestions: 📱: 400 @@ -1419,9 +1404,7 @@ contrat salarié . rémunération . avantages en nature . ntic . coût appareils contrat salarié . rémunération . avantages en nature . ntic . abonnements: question: Quel est le coût de l'abonnement (forfait mobile, etc.) pris en charge par l'employeur ? - unité: €/mois - - par défaut: 20 + par défaut: 20 €/mois suggestions: aucun: 0 standard: 20 @@ -1438,24 +1421,17 @@ contrat salarié . rémunération . avantages en nature . nourriture: contrat salarié . rémunération . avantages en nature . nourriture . montant: titre: nourriture unité: €/mois - formule: produit: - assiette: montant forfaitaire d'un repas + assiette [ref repas forfaitaire]: 4.85 €/repas facteur: repas par mois - -contrat salarié . rémunération . avantages en nature . nourriture . montant forfaitaire d'un repas: - unité: €/repas - - formule: 4.85 références: urssaf.fr: https://www.urssaf.fr/portail/home/taux-et-baremes/avantages-en-nature/nourriture.html contrat salarié . rémunération . avantages en nature . nourriture . repas par mois: question: > Combien de repas par mois sont payés par l'employeur ? - par défaut: 21 - unité: repas/mois + par défaut: 21 repas/mois suggestions: 1 par jour: 21 2 par jour: 42 @@ -1518,7 +1494,6 @@ contrat salarié . SMIC: Les heures supplémentaires et les heures complémentaires sont prises en compte sans tenir compte de la majoration. - unité par défaut: €/mois formule: temps de travail * SMIC horaire références: Détermination du SMIC: https://www.urssaf.fr/portail/home/employeur/beneficier-dune-exoneration/exonerations-generales/la-reduction-generale/le-calcul-de-la-reduction/etape-1--determination-du-coeffi/determination-du-smic-a-prendre.html @@ -1591,7 +1566,6 @@ contrat salarié . rémunération . net avec revenus de remplacement: contrat salarié . rémunération . net imposable: titre: Salaire net imposable type: salaire - unité par défaut: €/mois description: | C'est la base utilisée pour calculer l'impôt sur le revenu. formule: @@ -1643,7 +1617,7 @@ contrat salarié . prime d'impatriation: contrat salarié . rémunération . net: titre: Salaire net - unité par défaut: €/mois + unité: €/mois type: salaire question: Quel est votre salaire net ? résumé: Salaire net avant impôt @@ -1670,7 +1644,7 @@ contrat salarié . rémunération . net après impôt: résumé: Versé sur le compte bancaire question: Quel est le revenu net du salarié après impôt ? type: salaire - unité par défaut: €/mois + unité: €/mois description: | Le 1er janvier 2019, l'impôt sur le revenu est prélevé à la source et apparaît donc sur la fiche de paie. @@ -1698,14 +1672,14 @@ contrat salarié . prix du travail: - rémunération . total - (- aides employeur) - médecine du travail - unité par défaut: €/mois + unité: €/mois contrat salarié . rémunération . total: titre: Total chargé question: Quelle est la rémunération chargée ? résumé: Dépensé par l'entreprise type: salaire - unité par défaut: €/mois + unité: €/mois description: | C'est le total que l'employeur doit verser pour employer un salarié. formule: @@ -1737,7 +1711,7 @@ contrat salarié . cotisations . patronales . réductions de cotisations . dédu contrat salarié . réduction ACRE: applicable si: toutes ces conditions: - - dirigeant = 'assimilé salarié' + - dirigeant . assimilé salarié - entreprise . ACRE formule: produit: @@ -1797,14 +1771,12 @@ contrat salarié . cotisations: contrat salarié . cotisations . salariales . conventionnelles: titre: cotisations salariales conventionnelles description: Cotisations spécifiques à la convention collective - unité par défaut: €/mois - formule: 0 + formule: 0 €/mois contrat salarié . cotisations . patronales . conventionnelles: titre: cotisations patronales conventionnelles description: Cotisations spécifiques à la convention collective - unité par défaut: €/mois - formule: 0 + formule: 0 €/mois contrat salarié . cotisations . maladie sur les revenus de remplacement: formule: @@ -1934,8 +1906,7 @@ contrat salarié . temps de travail . temps partiel: par défaut: non contrat salarié . temps de travail . temps partiel . heures par semaine: - par défaut: 32 - unité: heures/semaine + par défaut: 32 heures/semaine question: Quel est le nombre d'heures travaillées par semaine dans le cadre du temps partiel ? contrôles: - si: @@ -1966,8 +1937,7 @@ contrat salarié . temps de travail . heures supplémentaires: titre: Nombre d'heures supplémentaires non applicable si: temps partiel question: Combien d'heures supplémentaires (non récupérées en repos) sont effectuées par mois ? - par défaut: 0 - unité: heure/mois + par défaut: 0 heure/mois suggestions: aucune: 0 39h / semaine: 17.33 @@ -2013,8 +1983,7 @@ contrat salarié . temps de travail . heures complémentaires: durée légale ou conventionnelle du travail. applicable si: temps partiel question: Combien d'heures complémentaires (non récupérées en repos) sont effectuées par mois ? - unité: heure/mois - par défaut: 0 + par défaut: 0 heure/mois contrôles: - si: heures complémentaires > seuil légal niveau: information @@ -2460,11 +2429,10 @@ contrat salarié . complémentaire santé: contrat salarié . complémentaire santé . part employeur: description: Part de la complémentaire santé payée par l'employeur. Doit être de 50% minimum question: Quelle est la part de la complémentaire santé payée par l'employeur ? - unité: '%' suggestions: 50%: 50 100%: 100 - par défaut: 50 + par défaut: 50% contrôles: - si: part employeur < 50% niveau: avertissement @@ -2476,7 +2444,6 @@ contrat salarié . complémentaire santé . part salarié: contrat salarié . complémentaire santé . forfait: titre: Forfait de complémentaire santé entreprise - unité: €/mois description: >- L'employeur a l'obligation de proposer une offre de complémentaire santé. Il doit prendre à sa charge au moins la moitié de son coût. @@ -2496,7 +2463,7 @@ contrat salarié . complémentaire santé . forfait: Alsace-moselle étude Meilleureassurance.com: http://www.lefigaro.fr/conjoncture/2018/10/16/20002-20181016ARTFIG00248-les-tarifs-des-complementaires-sante-font-le-grand-ecart-d-un-departement-a-l-autre.php question: Quel est le montant mensuel total (salarié et employeur) de la complémentaire santé entreprise ? - par défaut: 40 + par défaut: 40 €/mois suggestions: basique: 40 élevé: 100 @@ -3073,7 +3040,6 @@ contrat salarié . taxe sur les salaires . assiette de base: assiette: http://bofip.impots.gouv.fr/bofip/6690-PGP.html contrat salarié . taxe sur les salaires . assiette: - unité par défaut: €/mois formule: allègement: assiette: assiette de base @@ -3203,6 +3169,7 @@ contrat salarié . régime des impatriés: Article 155B du Code général des impôts: https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000006069577&idArticle=LEGIARTI000006307476&dateTexte=&categorieLien=cid contrat salarié . taxe sur les salaires: + unité: €/mois taxe: dû par: employeur description: La taxe sur les salaires en France est un impôt progressif créé en 1948 que certains employeurs doivent acquitter sur les salaires qu'ils distribuent. @@ -3217,7 +3184,7 @@ contrat salarié . taxe sur les salaires: - nom: non applicable par défaut situation: rémunération . brut de base: 2300 - valeur attendue: 0 + valeur attendue: false - nom: association non lucrative unipersonnelle situation: entreprise . association non lucrative: oui @@ -3401,7 +3368,8 @@ contrat salarié . lodeom . éligible barème compétitivité: Fiche URSSAF: https://www.urssaf.fr/portail/home/outre-mer/employeur/exoneration-de-cotisations-di-1/employeurs-situes-en-guadeloupe/bareme-dit-de-competitivite.html contrat salarié . lodeom . secteurs d'activité: - question: Votre entreprise appartient-elle à l'un de ces secteurs ? + applicable si: zone un + question: Votre entreprise appartient-elle à l'un des secteurs éligible LODEOM ? description: | Pour être éligible au 1er barème de l'exonération LODEOM, dit barème de compétitivité, votre entreprise doit appartenir à l'un des secteurs suivants : @@ -3536,7 +3504,7 @@ contrat salarié . cotisations . assiette forfaitaire . rémunération réelle: par défaut: non contrat salarié . convention collective: - par défaut: droit commun + par défaut: "'droit commun'" question: "Quelle convention collective est applicable à l'entreprise ? [beta] " formule: une possibilité: diff --git a/source/selectors/analyseSelectors.ts b/source/selectors/analyseSelectors.ts deleted file mode 100644 index 3d2a6737b..000000000 --- a/source/selectors/analyseSelectors.ts +++ /dev/null @@ -1,349 +0,0 @@ -import Engine, { parseRules } from 'Engine' -import { getNextSteps } from 'Engine/generateQuestions' -import { - collectDefaults, - disambiguateRuleReference, - splitName -} from 'Engine/ruleUtils' -import { ParsedRules } from 'Engine/types' -import { - add, - difference, - equals, - fromPairs, - head, - intersection, - isNil, - last, - length, - map, - mergeDeepWith, - negate, - pick, - pipe, - sortBy, - takeWhile, - toPairs, - zipWith -} from 'ramda' -import { useSelector } from 'react-redux' -import { RootState, Simulation } from 'Reducers/rootReducer' -import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect' -import { DottedName } from 'Rules' -import { mapOrApply } from '../utils' -// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle, -// comme dans sa formule -let disambiguateExampleSituation: any = (rules, rule) => - pipe( - toPairs as any, - map(([k, v]) => [ - disambiguateRuleReference(rules, rule.dottedName, k), - v - ]) as any, - fromPairs - ) -// create a "selector creator" that uses deep equal instead of === -const createDeepEqualSelector = createSelectorCreator(defaultMemoize, equals) - -let configSelector = (state: RootState) => state.simulation?.config || {} - -// We used to systematically put the rules in the Redux state but it broke hot -// reloading (the Redux store was re-created every time we changed the rules, -// and the situation was reseted). We now support both putting the rules in the -// state (for tests, library, etc.), and we default to a side-effect value (for -// hot-reloading on developement). See -// https://github.com/betagouv/mon-entreprise/issues/912 -// TOTO -let flatRulesSelector = (state: RootState) => state.rules - -// We must here compute parsedRules, flatRules, analyse which contains both -// targets and cache objects -export let parsedRulesSelector = createSelector( - [flatRulesSelector], - rules => parseRules(rules) as ParsedRules -) - -export let ruleDefaultsSelector = createSelector( - [parsedRulesSelector], - parsedRules => collectDefaults(parsedRules) -) - -export let targetNamesSelector = (state: RootState) => { - let objectifs = configSelector(state).objectifs - if (!objectifs || !Array.isArray(objectifs)) { - return [] - } - const targetNames = [].concat( - ...(objectifs as any).map(objectifOrGroup => - typeof objectifOrGroup === 'string' - ? [objectifOrGroup] - : objectifOrGroup.objectifs - ) - ) - - const secondaryTargetNames = - configSelector(state)['objectifs secondaires'] || [] - - return [...targetNames, ...secondaryTargetNames] -} - -type SituationSelectorType = typeof situationSelector - -export const situationSelector = (state: RootState) => - (state.simulation && state.simulation.situation) || {} - -export const useTarget = (dottedName: DottedName) => { - const targets = useSelector( - (state: RootState) => analysisWithDefaultsSelector(state).targets - ) - return targets && targets.find(t => t.dottedName === dottedName) -} - -export let noUserInputSelector = (state: RootState) => - !Object.keys(situationSelector(state)).length - -export let firstStepCompletedSelector = createSelector( - [situationSelector, targetNamesSelector, parsedRulesSelector, configSelector], - (situation, targetNames, parsedRules) => { - if (!situation) { - return true - } - const targetIsAnswered = targetNames?.some(targetName => { - const rule = parsedRules[targetName] - return rule?.formule && targetName in situation - }) - return targetIsAnswered - } -) - -let validatedStepsSelector = createSelector( - [state => state.simulation?.foldedSteps, targetNamesSelector], - (foldedSteps, targetNames) => [...(foldedSteps || []), ...targetNames] -) -export const defaultUnitSelector = (state: RootState) => - state.simulation?.defaultUnit ?? '€/mois' -let branchesSelector = (state: RootState) => configSelector(state).branches -let configSituationSelector = (state: RootState) => - configSelector(state).situation || {} - -const createSituationBrancheSelector = ( - situationSelector: SituationSelectorType -) => - createSelector( - [situationSelector, branchesSelector, configSituationSelector], - ( - situation, - branches, - configSituation - ): Simulation['situation'] | Array => { - if (branches) { - return branches.map(({ situation: branchSituation }) => ({ - ...configSituation, - ...branchSituation, - ...situation - })) - } - if (configSituation) { - return { ...configSituation, ...situation } - } - return situation || {} - } - ) - -export let situationBranchesSelector = createSituationBrancheSelector( - situationSelector -) -export let situationBranchNameSelector = createSelector( - [branchesSelector, state => state.situationBranch], - (branches, currentBranch) => - branches && !isNil(currentBranch) && branches[currentBranch].nom -) - -export let validatedSituationSelector = createSelector( - [situationSelector, validatedStepsSelector], - (situation, validatedSteps) => pick(validatedSteps, situation) -) -export let validatedSituationBranchesSelector = createSituationBrancheSelector( - validatedSituationSelector -) - -let evaluateRule = (parsedRules, ruleDottedName, situation, defaultUnits) => - new Engine({ rules: parsedRules }) - .setDefaultUnits(defaultUnits) - .setSituation(situation) - .evaluate(ruleDottedName) - -export let ruleAnalysisSelector = createSelector( - [ - parsedRulesSelector, - (_, props: { dottedName: DottedName }) => props.dottedName, - situationBranchesSelector, - state => state.situationBranch || 0, - defaultUnitSelector - ], - (rules, dottedName, situations, situationBranch, defaultUnit) => { - return evaluateRule( - rules, - dottedName, - Array.isArray(situations) ? situations[situationBranch] : situations, - [defaultUnit] - ) - } -) - -let exampleSituationSelector = createSelector( - [ - parsedRulesSelector, - situationBranchesSelector, - ({ currentExample }) => currentExample - ], - (rules, situations, example) => - example && { - ...(situations[0] || situations), - ...disambiguateExampleSituation( - rules, - rules[example.dottedName] - )(example.situation) - } -) -export let exampleAnalysisSelector = createSelector( - [ - parsedRulesSelector, - (_, props: { dottedName: DottedName }) => props.dottedName, - exampleSituationSelector, - ({ currentExample }) => currentExample - ], - (rules, dottedName, situation, example) => - situation && - evaluateRule(rules, dottedName, situation, example?.defaultUnit) -) - -let makeAnalysisSelector = ( - situationSelector: SituationSelectorType, - useDefaultValues -) => - createDeepEqualSelector( - [ - parsedRulesSelector, - targetNamesSelector, - situationSelector, - defaultUnitSelector - ], - (parsedRules, targetNames, situations, defaultUnit) => { - return mapOrApply(situation => { - const engine = new Engine({ rules: parsedRules, useDefaultValues }) - .setSituation(situation) - .setDefaultUnits([defaultUnit]) - return { - targets: targetNames.map(target => engine.evaluate(target)), - cache: engine.getCache(), - controls: engine.controls() - } - }, situations) - } - ) - -export let analysisWithDefaultsSelector = makeAnalysisSelector( - situationBranchesSelector as any, - true -) - -export let branchAnalyseSelector = createSelector( - [ - analysisWithDefaultsSelector, - (_, props: { situationBranchName: string }) => props?.situationBranchName, - branchesSelector - ], - (analysedSituations, branchName, branches) => { - if (!Array.isArray(analysedSituations) || !branchName || !branches) { - return analysedSituations - } - const branchIndex = branches.findIndex(branch => branch.nom === branchName) - return analysedSituations[branchIndex] - } -) - -let analysisValidatedOnlySelector = makeAnalysisSelector( - validatedSituationBranchesSelector as SituationSelectorType, - false -) - -let currentMissingVariablesByTargetSelector = createSelector( - [analysisValidatedOnlySelector], - analyses => { - const variables = mapOrApply( - analysis => - analysis.targets.reduce( - (acc, target) => ({ - [target.dottedName]: target.missingVariables, - ...acc - }), - {} - ), - analyses - ) - if (Array.isArray(variables)) { - return variables.reduce((acc, next) => mergeDeepWith(add)(acc, next), {}) - } - - return variables - } -) - -const similarity = (rule1: string = '', rule2: string = '') => - pipe( - zipWith(equals), - takeWhile(Boolean), - length, - negate - )(splitName(rule1), splitName(rule2)) - -export let nextStepsSelector = createSelector( - [ - currentMissingVariablesByTargetSelector, - configSelector, - (state: RootState) => state.simulation?.foldedSteps, - situationSelector - ], - ( - mv, - { - questions: { - 'non prioritaires': notPriority = [], - uniquement: only = null, - 'liste noire': blacklist = [] - } = {} - }, - foldedSteps = [], - situation - ) => { - let nextSteps = difference(getNextSteps(mv), foldedSteps) - - if (only) nextSteps = intersection(nextSteps, [...only, ...notPriority]) - if (blacklist) { - nextSteps = difference(nextSteps, blacklist) - } - - // L'ajout de la réponse permet de traiter les questions dont la réponse est "une possibilité", exemple "contrat salarié . cdd" - let lastStep = last(foldedSteps), - lastStepWithAnswer = - lastStep && situation[lastStep] - ? ([lastStep, situation[lastStep]].join(' . ') as DottedName) - : lastStep - - nextSteps = sortBy( - question => - notPriority.includes(question) - ? notPriority.indexOf(question) - : similarity(question, lastStepWithAnswer), - - nextSteps - ) - return nextSteps - } -) - -export let currentQuestionSelector = createSelector( - [nextStepsSelector, state => state.simulation?.unfoldedStep], - (nextSteps, unfoldedStep) => unfoldedStep || head(nextSteps) -) diff --git a/source/selectors/companyStatusSelectors.ts b/source/selectors/companyStatusSelectors.ts index 492f51128..75e41a07e 100644 --- a/source/selectors/companyStatusSelectors.ts +++ b/source/selectors/companyStatusSelectors.ts @@ -1,4 +1,4 @@ -import { SitePaths } from 'Components/utils/withSitePaths' +import { SitePaths } from 'Components/utils/SitePathsContext' import { add, any, diff --git a/source/selectors/ficheDePaieSelectors.ts b/source/selectors/ficheDePaieSelectors.ts deleted file mode 100644 index d2b2a5daa..000000000 --- a/source/selectors/ficheDePaieSelectors.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - add, - concat, - filter, - groupBy, - map, - mergeWith, - mergeWithKey, - path, - pathOr, - pipe, - prop, - reduce, - values -} from 'ramda' -import { createSelector } from 'reselect' -import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' -import { Branch, Cotisation } from './repartitionSelectors' -// Used for type consistency -export const BLANK_COTISATION: Cotisation = { - montant: { - partPatronale: 0, - partSalariale: 0 - }, - dottedName: 'ERROR_SHOULD_BE_INSTANCIATED' as any, - title: 'ERROR_SHOULD_BE_INSTANCIATED', - branche: 'protection sociale . autres' -} - -export const COTISATION_BRANCHE_ORDER: Array = [ - 'protection sociale . santé', - 'protection sociale . accidents du travail et maladies professionnelles', - 'protection sociale . retraite', - 'protection sociale . famille', - 'protection sociale . assurance chômage', - 'protection sociale . formation', - 'protection sociale . transport', - 'protection sociale . autres' -] - -function duParSelector(variable): 'employeur' | 'salarié' { - const dusPar = [ - ['cotisation', 'dû par'], - ['taxe', 'dû par'], - ['explanation', 'cotisation', 'dû par'], - ['explanation', 'taxe', 'dû par'] - ].map(p => path(p, variable)) - return dusPar.filter(Boolean)[0] as any -} -function brancheSelector(variable): Branch { - const branches = [ - ['cotisation', 'branche'], - ['taxe', 'branche'], - ['explanation', 'cotisation', 'branche'], - ['explanation', 'taxe', 'branche'] - ].map(p => path(p, variable)) - return ('protection sociale . ' + - (branches.filter(Boolean)[0] || 'autres')) as any -} - -export const mergeCotisations: ( - a: Cotisation, - b: Cotisation -) => Cotisation = mergeWithKey((key, a, b) => - key === 'montant' ? mergeWith(add, a, b) : b -) - -const variableToCotisation = (variable): Cotisation => { - return mergeCotisations(BLANK_COTISATION, { - ...variable.explanation, - branche: brancheSelector(variable), - montant: { - [duParSelector(variable) === 'salarié' - ? 'partSalariale' - : 'partPatronale']: variable.nodeValue - } - }) -} -const groupByBranche = (cotisations: Array) => { - const cotisationsMap = cotisations.reduce( - (acc, cotisation) => ({ - ...acc, - [cotisation.branche]: [...(acc[cotisation.branche] || []), cotisation] - }), - {} - ) - return COTISATION_BRANCHE_ORDER.map(branche => [ - branche, - cotisationsMap[branche] - ]) -} -export let analysisToCotisations = (analysis: { cache: Cache }) => { - const variables = [ - 'contrat salarié . cotisations . salariales', - 'contrat salarié . cotisations . patronales' - ] - .map(name => analysis.cache[name]) - .map(pathOr([], ['explanation', 'formule', 'explanation', 'explanation'])) - .reduce(concat as any, []) - - const cotisations = pipe( - map((rule: any) => - // Following : weird logic to automatically handle negative negated value in sum - - rule.operationType === 'calculation' && - rule.operator === '−' && - rule.explanation[0].nodeValue === 0 - ? { ...rule.explanation[1], nodeValue: rule.nodeValue } - : rule - ), - groupBy(prop('dottedName')), - values, - map( - pipe( - map(variableToCotisation), - reduce(mergeCotisations, BLANK_COTISATION) - ) - ), - filter( - cotisation => - cotisation.montant.partPatronale !== 0 || - cotisation.montant.partSalariale !== 0 - ), - groupByBranche, - filter(([, brancheCotisation]) => !!brancheCotisation) - )(variables) - return cotisations -} -export const analysisToCotisationsSelector = createSelector( - [analysisWithDefaultsSelector as any], - analysisToCotisations -) diff --git a/source/selectors/progressSelectors.ts b/source/selectors/progressSelectors.ts deleted file mode 100644 index 3527e34bb..000000000 --- a/source/selectors/progressSelectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RootState } from 'Reducers/rootReducer' -import { nextStepsSelector } from './analyseSelectors' - -export const simulationProgressSelector = (state: RootState) => { - const numberQuestionAnswered = state.simulation?.foldedSteps.length || 0 - const numberQuestionLeft = nextStepsSelector(state).length - return numberQuestionAnswered / (numberQuestionAnswered + numberQuestionLeft) -} diff --git a/source/selectors/repartitionSelectors.ts b/source/selectors/repartitionSelectors.ts deleted file mode 100644 index 799850d52..000000000 --- a/source/selectors/repartitionSelectors.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { getRuleFromAnalysis } from 'Engine/ruleUtils' -import { ParsedRule } from 'Engine/types' -import { compose, filter, fromPairs, map, max, reduce, sort } from 'ramda' -import { createSelector } from 'reselect' -import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' -import { - analysisToCotisations, - BLANK_COTISATION, - mergeCotisations -} from './ficheDePaieSelectors' - -export type Cotisation = Partial & { - branche: Branch - montant: MontantPartagé -} - -type MontantPartagé = { - partSalariale: number - partPatronale: number -} - -export type Branch = - | 'protection sociale . santé' - | 'protection sociale . accidents du travail et maladies professionnelles' - | 'protection sociale . retraite' - | 'protection sociale . famille' - | 'protection sociale . assurance chômage' - | 'protection sociale . formation' - | 'protection sociale . transport' - | 'protection sociale . autres' - -const totalCotisations = (cotisations: Array): MontantPartagé => - cotisations.reduce(mergeCotisations, BLANK_COTISATION).montant - -const byMontantTotal = ( - a: [Branch, MontantPartagé], - b: [Branch, MontantPartagé] -): number => { - return ( - b[1].partPatronale + - b[1].partSalariale - - a[1].partPatronale - - a[1].partSalariale - ) -} -// TODO : refaire ça proprement dans le moteur -const REPARTITION_CSG: Partial> = { - 'protection sociale . famille': 0.85, - 'protection sociale . santé': 7.75, - // TODO: cette part correspond à l'amortissement de la dette de la sécurité sociale. - // On peut imaginer la partager à toute les composantes concernées - 'protection sociale . autres': 0.6 -} - -const répartition = analysis => { - let cotisations = fromPairs(analysisToCotisations(analysis) as any) - - const getRule = getRuleFromAnalysis(analysis), - salaireNet = getRule('contrat salarié . rémunération . net'), - salaireChargé = getRule('contrat salarié . rémunération . total'), - cotisationsRule = getRule('contrat salarié . cotisations'), - réductionsDeCotisations = getRule( - 'contrat salarié . cotisations . patronales . réductions de cotisations' - ) - let répartitionMap = map(totalCotisations as any, cotisations) as any - - return { - répartition: compose( - sort(byMontantTotal), - Object.entries as any, - filter( - ({ partPatronale, partSalariale }) => - Math.round(partPatronale + partSalariale) !== 0 - ) - )(répartitionMap), - total: cotisationsRule.nodeValue, - cotisations: cotisationsRule, - maximum: compose( - reduce(max, 0), - map(montant => montant.partPatronale + montant.partSalariale), - Object.values - )(répartitionMap), - salaireNet, - salaireChargé - } -} - -export default createSelector([analysisWithDefaultsSelector], répartition) diff --git a/source/selectors/simulationSelectors.ts b/source/selectors/simulationSelectors.ts new file mode 100644 index 000000000..127544573 --- /dev/null +++ b/source/selectors/simulationSelectors.ts @@ -0,0 +1,40 @@ +import { DottedName } from './../rules/index' +import { createSelector } from 'reselect' + +export const configSelector = state => state.simulation?.config ?? {} +export const objectifsSelector = createSelector([configSelector], config => { + const primaryObjectifs = ((config.objectifs ?? []) as any) + .map((obj: DottedName | { objectifs: Array }) => + typeof obj === 'string' ? [obj] : obj.objectifs + ) + .flat() + + const objectifs = [...primaryObjectifs, ...(config['objectifs cachés'] ?? [])] + return objectifs +}) +const emptySituation = {} +export const situationSelector = state => + state.simulation?.situation ?? emptySituation +export const configSituationSelector = state => + configSelector(state).situation ?? emptySituation + +export const firstStepCompletedSelector = createSelector( + [situationSelector, objectifsSelector], + (situation, objectifs) => { + if (!situation) { + return false + } + return objectifs.some(objectif => { + return objectif in situation + }) + } +) + +export const targetUnitSelector = state => + state.simulation?.targetUnit ?? '€/mois' + +export const currentQuestionSelector = state => + state.simulation?.unfoldedStep ?? null + +export const answeredQuestionsSelector = state => + state.simulation?.foldedSteps ?? [] diff --git a/source/sites/mon-entreprise.fr/App.tsx b/source/sites/mon-entreprise.fr/App.tsx index 92a39062a..fe425079b 100644 --- a/source/sites/mon-entreprise.fr/App.tsx +++ b/source/sites/mon-entreprise.fr/App.tsx @@ -1,12 +1,23 @@ import * as Sentry from '@sentry/browser' import Route404 from 'Components/Route404' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { + EngineProvider, + SituationProvider +} from 'Components/utils/EngineContext' +import { SitePathsContext } from 'Components/utils/SitePathsContext' +import Engine from 'Engine' import 'iframe-resizer' -import React, { useContext, useEffect } from 'react' +import React, { useContext, useEffect, useMemo } from 'react' import { Helmet } from 'react-helmet' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import { Route, Switch } from 'react-router-dom' import createSentryMiddleware from 'redux-sentry-middleware' +import { Rules } from 'Rules' +import { + configSituationSelector, + situationSelector +} from 'Selectors/simulationSelectors' import 'Ui/index.css' import Provider, { ProviderProps } from '../../Provider' import { @@ -70,13 +81,19 @@ const middlewares = [ type InFranceRouteProps = { basename: ProviderProps['basename'] language: ProviderProps['language'] - rules: NonNullable['rules'] + rules: Rules } function InFranceRoute({ basename, language, rules }: InFranceRouteProps) { useEffect(() => { getSessionStorage()?.setItem('lang', language) }, [language]) + + // Hot reload rules + if (process.env.NODE_ENV !== 'production' && language === 'fr') { + rules = rules + } + const paths = constructLocalizedSitePath(language) return ( { - persistEverything({ except: ['rules', 'simulation'] })(store) + persistEverything({ except: ['simulation'] })(store) persistSimulation(store) }} initialStore={{ ...retrievePersistedState(), - previousSimulation: retrievePersistedSimulation(), - rules + previousSimulation: retrievePersistedSimulation() }} > - + + + ) } -let RouterSwitch = () => { +const Router = () => { return ( <> {!inIframe() &&
    } @@ -116,46 +134,68 @@ let RouterSwitch = () => { const App = () => { const { t } = useTranslation() const sitePaths = useContext(SitePathsContext) + const userSituation = useSelector(situationSelector) + const configSituation = useSelector(configSituationSelector) + const situation = useMemo( + () => ({ + ...configSituation, + ...userSituation + }), + [configSituation, userSituation] + ) return ( -
    - - {/* Passing location down to prevent update blocking */} + +
    + + {/* Passing location down to prevent update blocking */} -
    -
    - - {redirects} - - - - - - - - - - - - - +
    +
    + + {redirects} + + + + + + + + + + + + + - - + + +
    + + {!inIframe() &&
    }
    - - {!inIframe() &&
    }
    -
    + ) } diff --git a/source/sites/mon-entreprise.fr/layout/Footer/Footer.css b/source/sites/mon-entreprise.fr/layout/Footer/Footer.css index ad7ed3f2b..560f8ba5d 100644 --- a/source/sites/mon-entreprise.fr/layout/Footer/Footer.css +++ b/source/sites/mon-entreprise.fr/layout/Footer/Footer.css @@ -36,7 +36,7 @@ text-transform: uppercase; font-size: 85%; color: var(--darkColor); - font-weight: 500; + font-weight: 600; } .footer__registerField { display: flex; diff --git a/source/sites/mon-entreprise.fr/layout/Footer/Footer.tsx b/source/sites/mon-entreprise.fr/layout/Footer/Footer.tsx index 74e389deb..c8e6b3b12 100644 --- a/source/sites/mon-entreprise.fr/layout/Footer/Footer.tsx +++ b/source/sites/mon-entreprise.fr/layout/Footer/Footer.tsx @@ -1,6 +1,6 @@ import PageFeedback from 'Components/Feedback/PageFeedback' import LegalNotice from 'Components/LegalNotice' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import { lensPath, view } from 'ramda' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' diff --git a/source/sites/mon-entreprise.fr/layout/Header.tsx b/source/sites/mon-entreprise.fr/layout/Header.tsx index fac62a808..bd0d9e51d 100644 --- a/source/sites/mon-entreprise.fr/layout/Header.tsx +++ b/source/sites/mon-entreprise.fr/layout/Header.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import logoEnSvg from 'Images/logo-mycompany.svg' import logoSvg from 'Images/logo.svg' import marianneSvg from 'Images/marianne.svg' diff --git a/source/sites/mon-entreprise.fr/layout/NewsBanner.tsx b/source/sites/mon-entreprise.fr/layout/NewsBanner.tsx index 37f4284d7..4da5e4026 100644 --- a/source/sites/mon-entreprise.fr/layout/NewsBanner.tsx +++ b/source/sites/mon-entreprise.fr/layout/NewsBanner.tsx @@ -1,5 +1,5 @@ import { useLocalStorage, writeStorage } from '@rehooks/local-storage' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' import { useTranslation } from 'react-i18next' diff --git a/source/sites/mon-entreprise.fr/middlewares/trackSimulatorActions.ts b/source/sites/mon-entreprise.fr/middlewares/trackSimulatorActions.ts index b5837350f..1b620ce6d 100644 --- a/source/sites/mon-entreprise.fr/middlewares/trackSimulatorActions.ts +++ b/source/sites/mon-entreprise.fr/middlewares/trackSimulatorActions.ts @@ -1,7 +1,4 @@ -import { - currentQuestionSelector, - situationSelector -} from 'Selectors/analyseSelectors' +import { situationSelector } from 'Selectors/simulationSelectors' import Tracker from 'Tracker' export default (tracker: Tracker) => { @@ -17,14 +14,15 @@ export default (tracker: Tracker) => { situationSelector(newState)[action.step] ]) - if (!currentQuestionSelector(newState)) { - tracker.push([ - 'trackEvent', - 'Simulator', - 'simulation completed', - 'after ' + newState.simulation.foldedSteps.length + ' questions' - ]) - } + // TODO : add tracking in UI instead ? + // if (!currentQuestionSelector(newState)) { + // tracker.push([ + // 'trackEvent', + // 'Simulator', + // 'simulation completed', + // 'after ' + newState.simulation.foldedSteps.length + ' questions' + // ]) + // } } if (action.type === 'SET_ACTIVE_TARGET_INPUT') { @@ -38,14 +36,14 @@ export default (tracker: Tracker) => { if ( action.type === 'UPDATE_SITUATION' || - action.type === 'UPDATE_DEFAULT_UNIT' + action.type === 'UPDATE_TARGET_UNIT' ) { tracker.push([ 'trackEvent', 'Simulator', 'update situation', - ...(action.type === 'UPDATE_DEFAULT_UNIT' - ? ['unité', action.defaultUnit] + ...(action.type === 'UPDATE_TARGET_UNIT' + ? ['unité', action.targetUnit] : [action.fieldName, action.value]) ]) } diff --git a/source/sites/mon-entreprise.fr/pages/Coronavirus.tsx b/source/sites/mon-entreprise.fr/pages/Coronavirus.tsx index b477d4ffa..8d0524522 100644 --- a/source/sites/mon-entreprise.fr/pages/Coronavirus.tsx +++ b/source/sites/mon-entreprise.fr/pages/Coronavirus.tsx @@ -3,19 +3,18 @@ import RuleLink from 'Components/RuleLink' import Simulation from 'Components/Simulation' import chomagePartielConfig from 'Components/simulationConfigs/chômage-partiel.yaml' import Warning from 'Components/ui/WarningBlock' -import { ThemeColorsContext } from 'Components/utils/colors' import { IsEmbeddedContext } from 'Components/utils/embeddedContext' +import { useEvaluation } from 'Components/utils/EngineContext' import { Markdown } from 'Components/utils/markdown' import { ScrollToTop } from 'Components/utils/Scroll' import { formatValue } from 'Engine/format' -import { getRuleFromAnalysis } from 'Engine/ruleUtils' +import { EvaluatedRule } from 'Engine/types' import React, { useContext, useEffect, useState } from 'react' import { Helmet } from 'react-helmet' import { Trans, useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { useLocation } from 'react-router' -import { EvaluatedRule } from 'Rules' -import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' +import { DottedName } from 'Rules' import styled from 'styled-components' import Animate from 'Ui/animate' import { productionMode } from '../../../utils' @@ -109,25 +108,22 @@ export default function ChômagePartiel() { } function ExplanationSection() { - const analysis = useSelector(analysisWithDefaultsSelector) const { i18n: { language }, t } = useTranslation() - const { palettes } = useContext(ThemeColorsContext) - const getRule = getRuleFromAnalysis(analysis) - const net = getRule('contrat salarié . rémunération . net') - const netHabituel = getRule('chômage partiel . revenu net habituel') - const totalEntreprise = getRule('contrat salarié . prix du travail') - const totalEntrepriseHabituel = getRule( + const net = useEvaluation('contrat salarié . rémunération . net') + const netHabituel = useEvaluation('chômage partiel . revenu net habituel') + const totalEntreprise = useEvaluation('contrat salarié . prix du travail') + const totalEntrepriseHabituel = useEvaluation( 'chômage partiel . coût employeur habituel' ) if ( - !net?.nodeValue || - !netHabituel?.nodeValue || - totalEntreprise?.nodeValue == null || - !totalEntrepriseHabituel?.nodeValue + typeof net?.nodeValue !== 'number' || + typeof netHabituel?.nodeValue !== 'number' || + typeof totalEntreprise?.nodeValue !== 'number' || + typeof totalEntrepriseHabituel?.nodeValue !== 'number' ) { return null } @@ -154,17 +150,19 @@ function ExplanationSection() { { ...net, additionalText: language === 'fr' && ( - <> + Soit{' '} {formatValue({ - value: (net.nodeValue / netHabituel.nodeValue) * 100, + nodeValue: + (net.nodeValue / netHabituel.nodeValue) * 100, unit: '%', - maximumFractionDigits: 0 + language: 'fr', + precision: 0 })} {' '} du revenu net - + ) } ], @@ -174,20 +172,21 @@ function ExplanationSection() { { ...totalEntreprise, additionalText: language === 'fr' && ( - <> + Soit{' '} {formatValue({ - value: + nodeValue: (totalEntreprise.nodeValue / totalEntrepriseHabituel.nodeValue) * 100, unit: '%', - maximumFractionDigits: 0 + language: 'fr', + precision: 0 })} {' '} du coût habituel - + ) } ] @@ -204,7 +203,7 @@ type ComparaisonTableProps = { } type Line = Array< - EvaluatedRule & { + EvaluatedRule & { additionalText?: React.ReactNode } > @@ -279,15 +278,15 @@ function ComparaisonTable({ rows: [head, ...body] }: ComparaisonTableProps) { ) } -function ValueWithLink(rule: EvaluatedRule) { +function ValueWithLink(rule: EvaluatedRule) { const { language } = useTranslation().i18n return ( {formatValue({ - value: rule.nodeValue as number, + nodeValue: rule.nodeValue as number, language, unit: '€', - maximumFractionDigits: 0 + precision: 0 })} ) diff --git a/source/sites/mon-entreprise.fr/pages/Créer/AfterRegistration.tsx b/source/sites/mon-entreprise.fr/pages/Créer/AfterRegistration.tsx index bd867e016..e817573ba 100644 --- a/source/sites/mon-entreprise.fr/pages/Créer/AfterRegistration.tsx +++ b/source/sites/mon-entreprise.fr/pages/Créer/AfterRegistration.tsx @@ -1,5 +1,5 @@ import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' diff --git a/source/sites/mon-entreprise.fr/pages/Créer/CreationChecklist.tsx b/source/sites/mon-entreprise.fr/pages/Créer/CreationChecklist.tsx index e17b61ebb..4d069e57e 100644 --- a/source/sites/mon-entreprise.fr/pages/Créer/CreationChecklist.tsx +++ b/source/sites/mon-entreprise.fr/pages/Créer/CreationChecklist.tsx @@ -4,7 +4,7 @@ import { } from 'Actions/companyCreationChecklistActions' import { goToCompanyStatusChoice } from 'Actions/companyStatusActions' import Scroll from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' import { Helmet } from 'react-helmet' diff --git a/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/PickLegalStatus.tsx b/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/PickLegalStatus.tsx index 930a2ef31..9b055e452 100644 --- a/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/PickLegalStatus.tsx +++ b/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/PickLegalStatus.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import { filter } from 'ramda' import React, { useContext } from 'react' import { Helmet } from 'react-helmet' diff --git a/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/PreviousAnswers.tsx b/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/PreviousAnswers.tsx index 7a88e4356..eab706712 100644 --- a/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/PreviousAnswers.tsx +++ b/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/PreviousAnswers.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import { isNil } from 'ramda' import React, { useContext } from 'react' import { Trans } from 'react-i18next' diff --git a/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/index.tsx b/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/index.tsx index d0812a4d7..ee0cee966 100644 --- a/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/index.tsx +++ b/source/sites/mon-entreprise.fr/pages/Créer/GuideStatut/index.tsx @@ -1,5 +1,5 @@ import { resetCompanyStatusChoice } from 'Actions/companyStatusActions' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import { toPairs } from 'ramda' import React, { useContext, useEffect } from 'react' import { Trans } from 'react-i18next' diff --git a/source/sites/mon-entreprise.fr/pages/Créer/Home.tsx b/source/sites/mon-entreprise.fr/pages/Créer/Home.tsx index 5bccca196..78728d224 100644 --- a/source/sites/mon-entreprise.fr/pages/Créer/Home.tsx +++ b/source/sites/mon-entreprise.fr/pages/Créer/Home.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Helmet } from 'react-helmet' import { Trans, useTranslation } from 'react-i18next' diff --git a/source/sites/mon-entreprise.fr/pages/Créer/index.tsx b/source/sites/mon-entreprise.fr/pages/Créer/index.tsx index cfea9bba9..acfd2c859 100644 --- a/source/sites/mon-entreprise.fr/pages/Créer/index.tsx +++ b/source/sites/mon-entreprise.fr/pages/Créer/index.tsx @@ -1,5 +1,5 @@ import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Route, Switch } from 'react-router' import { useLocation } from 'react-router-dom' diff --git a/source/sites/mon-entreprise.fr/pages/Dev/Sitemap.tsx b/source/sites/mon-entreprise.fr/pages/Dev/Sitemap.tsx index ff73eaccd..273a30d64 100644 --- a/source/sites/mon-entreprise.fr/pages/Dev/Sitemap.tsx +++ b/source/sites/mon-entreprise.fr/pages/Dev/Sitemap.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { generateSiteMap, SitePathsType } from '../../sitePaths' diff --git a/source/sites/mon-entreprise.fr/pages/Documentation.tsx b/source/sites/mon-entreprise.fr/pages/Documentation.tsx new file mode 100644 index 000000000..1e6d34cb7 --- /dev/null +++ b/source/sites/mon-entreprise.fr/pages/Documentation.tsx @@ -0,0 +1,20 @@ +import Documentation from 'Components/Documentation' +import { EngineContext } from 'Components/utils/EngineContext' +import { SitePathsContext } from 'Components/utils/SitePathsContext' +import React, { useContext } from 'react' +import { useLocation } from 'react-router' + +export default function SiteDocumentation() { + const engine = useContext(EngineContext) + const sitePaths = useContext(SitePathsContext) + const useDefaultValues = + (useLocation().state as { useDefaultValues?: boolean })?.useDefaultValues ?? + false + return ( + + ) +} diff --git a/source/sites/mon-entreprise.fr/pages/Documentation/index.tsx b/source/sites/mon-entreprise.fr/pages/Documentation/index.tsx deleted file mode 100644 index 234a6984a..000000000 --- a/source/sites/mon-entreprise.fr/pages/Documentation/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import RulePage from 'Components/RulePage' -import { SitePathsContext } from 'Components/utils/withSitePaths' -import React, { useContext } from 'react' -import { Route, Switch } from 'react-router' -import RulesList from './RulesList' - -export default function Documentation() { - const sitePaths = useContext(SitePathsContext) - return ( - - - - - ) -} diff --git a/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Result.tsx b/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Result.tsx index 1d1bf5ac1..bf16db536 100644 --- a/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Result.tsx +++ b/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Result.tsx @@ -1,22 +1,20 @@ import RuleLink from 'Components/RuleLink' +import { useEvaluation } from 'Components/utils/EngineContext' import { formatValue } from 'Engine/format' import React from 'react' import emoji from 'react-easy-emoji' import { Trans } from 'react-i18next' import Skeleton from 'react-loading-skeleton' import ReactToPrint from 'react-to-print' -import { DottedName, EvaluatedRule } from 'Rules' import Animate from 'Ui/animate' -import { useRule } from '../../Simulateurs/ArtisteAuteur' import simulationConfig from './config.yaml' +import { DottedName } from 'Rules' type ResultsProp = { componentRef?: any } export function Results({ componentRef }: ResultsProp) { - const results: EvaluatedRule[] = simulationConfig.objectifs.map( - (dottedName: DottedName) => useRule(dottedName) - ) + const results = useEvaluation(simulationConfig.objectifs as Array) const onGoingComputation = !results.filter(node => node.nodeValue != null) .length return ( @@ -51,10 +49,10 @@ export function Results({ componentRef }: ResultsProp) { {r.nodeValue != null ? ( formatValue({ - value: r.nodeValue || 0, + nodeValue: r.nodeValue || 0, language: 'fr', unit: '€', - maximumFractionDigits: 0 + precision: 0 }) ) : ( diff --git a/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx b/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx deleted file mode 100644 index c9c50975d..000000000 --- a/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import CompanyDetails from 'Components/CompanyDetails' -import { formatValue } from 'Engine/format' -import React, { useRef } from 'react' -import { Trans } from 'react-i18next' -import { useSelector } from 'react-redux' -import { RootState } from 'Reducers/rootReducer' -import { DottedName } from 'Rules' -import { situationSelector } from 'Selectors/analyseSelectors' -import { Results } from './Result' - -export function AideDéclarationIndépendantsRécapitulatif() { - const situation = useSelector(situationSelector) - const siren = useSelector( - (state: RootState) => state.inFranceApp.existingCompany?.siren - ) - const componentRef = useRef(null) - - return ( -
    -

    - Aide à la déclaration de revenus au titre de l'année 2019 -

    - -

    - Ce document atteste de votre bonne foi concernant votre déclaration - selon les éléments transmis. -

    - -

    - Récapitulatif -

    - - - - {siren && } - - - - - - {!situation[ - "situation personnelle . domiciliation fiscale à l'étranger" - ] && ( - <> - - - - - - - )} - - - - - - - - - - - - -
    - ) -} - -type SimpleFieldProps = { - dottedName: DottedName - unit?: string -} -function SimpleField({ dottedName, unit }: SimpleFieldProps) { - const situation = useSelector(situationSelector) - const rules = useSelector((state: RootState) => state.rules) - const value = situation[dottedName] - return value && (value === 'oui' || unit === '€') ? ( -

    - {rules[dottedName]?.question} - -   - - {value !== null && unit === '€' ? ( - formatValue({ - value: value || 0, - language: 'fr', - unit: unit, - maximumFractionDigits: 0 - }) - ) : ( - <>{value} - )} - - -

    - ) : null -} diff --git a/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/index.tsx b/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/index.tsx index 9307ca50c..975a9a864 100644 --- a/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/index.tsx +++ b/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/index.tsx @@ -3,27 +3,29 @@ import Aide from 'Components/conversation/Aide' import Explicable from 'Components/conversation/Explicable' import 'Components/TargetSelection.css' import Warning from 'Components/ui/WarningBlock' +import { useEvaluation, EngineContext } from 'Components/utils/EngineContext' import { ScrollToTop } from 'Components/utils/Scroll' import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting' import RuleInput from 'Engine/RuleInput' import { ParsedRule } from 'Engine/types' -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { + useCallback, + useEffect, + useRef, + useState, + useContext +} from 'react' import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' import { DottedName } from 'Rules' -import { - nextStepsSelector, - parsedRulesSelector, - ruleAnalysisSelector, - situationSelector -} from 'Selectors/analyseSelectors' +import { situationSelector } from 'Selectors/simulationSelectors' import styled from 'styled-components' import Animate from 'Ui/animate' -import { useRule } from '../../Simulateurs/ArtisteAuteur' import { CompanySection } from '../Home' import simulationConfig from './config.yaml' import { Results } from './Result' +import { useNextQuestions } from 'Components/utils/useNextQuestion' const lauchComputationWhenResultsInViewport = () => { const dottedName = 'dirigeant . rémunération totale' @@ -55,7 +57,8 @@ const lauchComputationWhenResultsInViewport = () => { export default function AideDéclarationIndépendant() { const dispatch = useDispatch() - const rules = useSelector(parsedRulesSelector) + const rules = useContext(EngineContext).getParsedRules() + const company = useSelector( (state: RootState) => state.inFranceApp.existingCompany ) @@ -214,9 +217,9 @@ function SubSection({ dottedName: sectionDottedName, hideTitle = false }: SubSectionProp) { - const parsedRules = useSelector(parsedRulesSelector) - const ruleTitle = useRule(sectionDottedName)?.title - const nextSteps = useSelector(nextStepsSelector) + const parsedRules = useContext(EngineContext).getParsedRules() + const ruleTitle = parsedRules[sectionDottedName]?.title + const nextSteps = useNextQuestions() const situation = useSelector(situationSelector) const title = hideTitle ? null : ruleTitle const subQuestions = Object.values(parsedRules).filter( @@ -243,10 +246,8 @@ type SimpleFieldProps = { } function SimpleField({ dottedName, question, summary }: SimpleFieldProps) { const dispatch = useDispatch() - const evaluatedRule = useSelector((state: RootState) => { - return ruleAnalysisSelector(state, { dottedName }) - }) - const rules = useSelector(parsedRulesSelector) + const evaluatedRule = useEvaluation(dottedName) + const rules = useContext(EngineContext).getParsedRules() const value = useSelector(situationSelector)[dottedName] const [currentValue, setCurrentValue] = useState(value) const dispatchValue = useCallback( diff --git a/source/sites/mon-entreprise.fr/pages/Gérer/Home.tsx b/source/sites/mon-entreprise.fr/pages/Gérer/Home.tsx index 9c865a6de..2f9e2fd31 100644 --- a/source/sites/mon-entreprise.fr/pages/Gérer/Home.tsx +++ b/source/sites/mon-entreprise.fr/pages/Gérer/Home.tsx @@ -7,7 +7,7 @@ import CompanyDetails from 'Components/CompanyDetails' import FindCompany from 'Components/FindCompany' import Overlay from 'Components/Overlay' import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext, useEffect, useRef, useState } from 'react' import emoji from 'react-easy-emoji' import { Helmet } from 'react-helmet' diff --git a/source/sites/mon-entreprise.fr/pages/Gérer/SchemeSelection.tsx b/source/sites/mon-entreprise.fr/pages/Gérer/SchemeSelection.tsx index d84fc7c3f..10a9883a9 100644 --- a/source/sites/mon-entreprise.fr/pages/Gérer/SchemeSelection.tsx +++ b/source/sites/mon-entreprise.fr/pages/Gérer/SchemeSelection.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' import { Helmet } from 'react-helmet' diff --git a/source/sites/mon-entreprise.fr/pages/Gérer/index.tsx b/source/sites/mon-entreprise.fr/pages/Gérer/index.tsx index 260b6dd40..0d9996ca2 100644 --- a/source/sites/mon-entreprise.fr/pages/Gérer/index.tsx +++ b/source/sites/mon-entreprise.fr/pages/Gérer/index.tsx @@ -1,11 +1,10 @@ import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Trans } from 'react-i18next' import { Route, Switch } from 'react-router' import { NavLink, useLocation } from 'react-router-dom' import AideDéclarationIndépendant from './AideDéclarationIndépendant/index' -import { AideDéclarationIndépendantsRécapitulatif } from './AideDéclarationIndépendant/Récapitulatif' import Embaucher from './Embaucher' import Home from './Home' import SécuritéSociale from './SécuritéSociale' @@ -16,28 +15,14 @@ export default function Gérer() { return ( <> -
    - {location.pathname === - '/gérer/aide-declaration-independants/récapitulatif' ? ( - - ← Retour à ma déclaration - - ) : ( - - ← Retour à mon activité - - )} -
    + + ← Retour à mon activité + - ) diff --git a/source/sites/mon-entreprise.fr/pages/Iframes/SimulateurEmbauche.tsx b/source/sites/mon-entreprise.fr/pages/Iframes/SimulateurEmbauche.tsx index 500b1f171..3886b56ab 100644 --- a/source/sites/mon-entreprise.fr/pages/Iframes/SimulateurEmbauche.tsx +++ b/source/sites/mon-entreprise.fr/pages/Iframes/SimulateurEmbauche.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Helmet } from 'react-helmet' import { SalarySimulation } from '../Simulateurs/Salarié' diff --git a/source/sites/mon-entreprise.fr/pages/Landing/Landing.tsx b/source/sites/mon-entreprise.fr/pages/Landing/Landing.tsx index 1dbb0bc7b..947ee94b7 100644 --- a/source/sites/mon-entreprise.fr/pages/Landing/Landing.tsx +++ b/source/sites/mon-entreprise.fr/pages/Landing/Landing.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import logoSvg from 'Images/logo.svg' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' diff --git a/source/sites/mon-entreprise.fr/pages/Nouveautés/Nouveautés.tsx b/source/sites/mon-entreprise.fr/pages/Nouveautés/Nouveautés.tsx index 79b5c13ca..7a3f99343 100644 --- a/source/sites/mon-entreprise.fr/pages/Nouveautés/Nouveautés.tsx +++ b/source/sites/mon-entreprise.fr/pages/Nouveautés/Nouveautés.tsx @@ -1,7 +1,7 @@ import MoreInfosOnUs from 'Components/MoreInfosOnUs' import { MarkdownWithAnchorLinks } from 'Components/utils/markdown' import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext, useEffect } from 'react' import emoji from 'react-easy-emoji' import { Redirect, useHistory, useRouteMatch } from 'react-router' diff --git a/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx b/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx index 6b4d3fd84..e8e9e5d76 100644 --- a/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx +++ b/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx @@ -1,33 +1,21 @@ import { setSimulationConfig, updateSituation } from 'Actions/actions' import { DistributionBranch } from 'Components/Distribution' -import RuleLink from 'Components/RuleLink' +import { Condition } from 'Components/EngineValue' import SimulateurWarning from 'Components/SimulateurWarning' import config from 'Components/simulationConfigs/artiste-auteur.yaml' import 'Components/TargetSelection.css' import { IsEmbeddedContext } from 'Components/utils/embeddedContext' -import { formatValue } from 'Engine/format' +import { EngineContext, useEvaluation } from 'Components/utils/EngineContext' +import Value from 'Components/EngineValue' import RuleInput from 'Engine/RuleInput' -import { getRuleFromAnalysis } from 'Engine/ruleUtils' import React, { createContext, useContext, useEffect, useState } from 'react' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { RootState } from 'Reducers/rootReducer' import { DottedName } from 'Rules' -import { - analysisWithDefaultsSelector, - parsedRulesSelector, - ruleAnalysisSelector, - situationSelector -} from 'Selectors/analyseSelectors' +import { situationSelector } from 'Selectors/simulationSelectors' import styled from 'styled-components' import Animate from 'Ui/animate' -export function useRule(dottedName: DottedName) { - const analysis = useSelector(analysisWithDefaultsSelector) - const getRule = getRuleFromAnalysis(analysis) - return getRule(dottedName) -} - const InitialRenderContext = createContext(false) function useInitialRender() { const [initialRender, setInitialRender] = useState(true) @@ -77,16 +65,12 @@ type SimpleFieldProps = { } function SimpleField({ dottedName }: SimpleFieldProps) { - const rule = useSelector(parsedRulesSelector)[dottedName] const dispatch = useDispatch() - const analysis = useSelector((state: RootState) => { - return ruleAnalysisSelector(state, { dottedName }) - }) + const rule = useEvaluation(dottedName) const initialRender = useContext(InitialRenderContext) - const parsedRules = useSelector(parsedRulesSelector) - const value = useSelector(situationSelector)[dottedName] - - if (!analysis.isApplicable) { + const parsedRules = useContext(EngineContext).getParsedRules() + const value = useSelector(situationSelector)[dottedName] ?? rule['par défaut'] + if (rule.isApplicable === false || rule.isApplicable === null) { return null } @@ -147,10 +131,7 @@ const ResultLabel = styled.div` function CotisationsResult() { const [display, setDisplay] = useState(false) - const { i18n } = useTranslation() const situation = useSelector(situationSelector) - const cotisationRule = useRule('artiste-auteur . cotisations') - const value = cotisationRule.nodeValue if (Object.keys(situation).length && !display) { setDisplay(true) @@ -166,16 +147,15 @@ function CotisationsResult() { Montant des cotisations - - {formatValue({ - value: cotisationRule.nodeValue, - language: i18n.language, - unit: '€', - maximumFractionDigits: 0 - })} - + - {cotisationRule.nodeValue ? : null} + + + ) } @@ -200,9 +180,10 @@ const branches = [ ] as const function RepartitionCotisations() { + const engine = useContext(EngineContext) const cotisations = branches.map(branch => ({ ...branch, - value: useRule(branch.dottedName).nodeValue as number + value: engine.evaluate(branch.dottedName).nodeValue as number })) const maximum = Math.max(...cotisations.map(x => x.value)) return ( diff --git a/source/sites/mon-entreprise.fr/pages/Simulateurs/AutoEntrepreneur.tsx b/source/sites/mon-entreprise.fr/pages/Simulateurs/AutoEntrepreneur.tsx index 96d705564..a9e76da9b 100644 --- a/source/sites/mon-entreprise.fr/pages/Simulateurs/AutoEntrepreneur.tsx +++ b/source/sites/mon-entreprise.fr/pages/Simulateurs/AutoEntrepreneur.tsx @@ -5,13 +5,12 @@ import autoEntrepreneurConfig from 'Components/simulationConfigs/auto-entreprene import StackedBarChart from 'Components/StackedBarChart' import { ThemeColorsContext } from 'Components/utils/colors' import { IsEmbeddedContext } from 'Components/utils/embeddedContext' -import { getRuleFromAnalysis } from 'Engine/ruleUtils' +import { EngineContext } from 'Components/utils/EngineContext' import { default as React, useContext } from 'react' import { Helmet } from 'react-helmet' import { Trans, useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { useLocation } from 'react-router' -import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' export default function AutoEntrepreneur() { const dispatch = useDispatch() @@ -54,12 +53,11 @@ export default function AutoEntrepreneur() { } function ExplanationSection() { - const analysis = useSelector(analysisWithDefaultsSelector) - const getRule = getRuleFromAnalysis(analysis) + const engine = useContext(EngineContext) const { t } = useTranslation() const { palettes } = useContext(ThemeColorsContext) - const impôt = getRule('impôt') + const impôt = engine.evaluate('impôt') return (

    @@ -68,7 +66,9 @@ function ExplanationSection() { { const dispatch = useDispatch() const location = useLocation<{ fromGérer?: boolean }>() - dispatch(setSimulationConfig(salariéConfig, location.state?.fromGérer)) const sitePaths = useContext(SitePathsContext) + dispatch(setSimulationConfig(salariéConfig, location.state?.fromGérer)) return ( <> diff --git a/source/sites/mon-entreprise.fr/pages/integration/Options.tsx b/source/sites/mon-entreprise.fr/pages/integration/Options.tsx index 8a0f886f8..a9f55561e 100644 --- a/source/sites/mon-entreprise.fr/pages/integration/Options.tsx +++ b/source/sites/mon-entreprise.fr/pages/integration/Options.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' import { Trans } from 'react-i18next' diff --git a/source/sites/mon-entreprise.fr/pages/integration/index.tsx b/source/sites/mon-entreprise.fr/pages/integration/index.tsx index 26d6bde0e..19030abca 100644 --- a/source/sites/mon-entreprise.fr/pages/integration/index.tsx +++ b/source/sites/mon-entreprise.fr/pages/integration/index.tsx @@ -1,5 +1,5 @@ import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Trans } from 'react-i18next' import { Route, Switch, useLocation } from 'react-router' diff --git a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/Activité.tsx b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/Activité.tsx index a959ceea4..c02c9c6e9 100644 --- a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/Activité.tsx +++ b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/Activité.tsx @@ -1,7 +1,7 @@ import { Markdown } from 'Components/utils/markdown' import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' -import Value from 'Components/Value' +import { SitePathsContext } from 'Components/utils/SitePathsContext' +import Value from 'Components/EngineValue' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' import { Trans, useTranslation } from 'react-i18next' @@ -14,6 +14,7 @@ import ExceptionsExonération from './ExceptionsExonération' import NextButton from './NextButton' import { estExonéréeSelector } from './selectors' import { StoreContext } from './StoreContext' +import { formatValue } from 'Engine/format' export default function Activité({ match: { @@ -110,9 +111,12 @@ export default function Activité({ defaultChecked={seuilRevenus === 'AUCUN'} />{' '} inférieurs à{' '} - - {activité['seuil déclaration']} - + {formatValue({ + nodeValue: activité['seuil déclaration'], + precision: 0, + language, + unit: '€' + })} )} @@ -125,9 +129,12 @@ export default function Activité({ defaultChecked={seuilRevenus === 'IMPOSITION'} />{' '} inférieurs à{' '} - - {activité['seuil pro']} - + {formatValue({ + nodeValue: activité['seuil pro'], + precision: 0, + language, + unit: '€' + })} {activité['seuil régime général'] && ( @@ -142,9 +149,12 @@ export default function Activité({ } />{' '} supérieurs à{' '} - - {activité['seuil pro']} - + {formatValue({ + nodeValue: activité['seuil pro'], + precision: 0, + language, + unit: '€' + })} )} @@ -160,9 +170,13 @@ export default function Activité({ } />{' '} supérieurs à{' '} - - {activité['seuil régime général'] || activité['seuil pro']} - + {formatValue({ + nodeValue: + activité['seuil régime général'] || activité['seuil pro'], + precision: 0, + language, + unit: '€' + })} diff --git a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/ActivitésSelection.tsx b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/ActivitésSelection.tsx index a1fbe8810..619187011 100644 --- a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/ActivitésSelection.tsx +++ b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/ActivitésSelection.tsx @@ -1,6 +1,6 @@ import classnames from 'classnames' import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import { intersection } from 'ramda' import React, { useCallback, useContext } from 'react' import emoji from 'react-easy-emoji' diff --git a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/NextButton.tsx b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/NextButton.tsx index 8061a2d9a..06d3df4ea 100644 --- a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/NextButton.tsx +++ b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/NextButton.tsx @@ -1,5 +1,5 @@ import classnames from 'classnames' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Trans } from 'react-i18next' import { Link } from 'react-router-dom' diff --git a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/VotreSituation.tsx b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/VotreSituation.tsx index 3476fff1b..8b97f6101 100644 --- a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/VotreSituation.tsx +++ b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/VotreSituation.tsx @@ -1,5 +1,5 @@ import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Helmet } from 'react-helmet' import { Trans, useTranslation } from 'react-i18next' diff --git a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/index.tsx b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/index.tsx index 6b9570bed..d06e3398b 100644 --- a/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/index.tsx +++ b/source/sites/mon-entreprise.fr/pages/ÉconomieCollaborative/index.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/withSitePaths' +import { SitePathsContext } from 'Components/utils/SitePathsContext' import React, { useContext } from 'react' import { Trans } from 'react-i18next' import { Route, Switch } from 'react-router' diff --git a/source/sites/mon-entreprise.fr/sitePaths.ts b/source/sites/mon-entreprise.fr/sitePaths.ts index ab719905b..f6b1e730f 100644 --- a/source/sites/mon-entreprise.fr/sitePaths.ts +++ b/source/sites/mon-entreprise.fr/sitePaths.ts @@ -93,10 +93,6 @@ export const constructLocalizedSitePath = (language: string) => { index: t( 'path.gérer.déclaration-indépendant.index', '/aide-declaration-independants' - ), - récapitulatif: t( - 'path.gérer.déclaration-indépendant.récapitulatif', - '/récapitulatif' ) } }, diff --git a/source/sites/publi.codes/Studio.tsx b/source/sites/publi.codes/Studio.tsx index 86fe3bd5c..eead76968 100644 --- a/source/sites/publi.codes/Studio.tsx +++ b/source/sites/publi.codes/Studio.tsx @@ -1,12 +1,13 @@ // import { ControlledEditor } from '@monaco-editor/react' +import Engine from 'Engine' import { formatValue } from 'Engine/format' -import Engine from 'Engine/react' import { safeLoad } from 'js-yaml' import { last } from 'ramda' import React, { useCallback, useEffect, useMemo, useState } from 'react' import emoji from 'react-easy-emoji' import MonacoEditor from 'react-monaco-editor' import { useLocation } from 'react-router' +import { DottedName } from 'Rules' import styled from 'styled-components' const EXAMPLE_CODE = ` @@ -83,7 +84,6 @@ export default function Studio() { setEditorValue(newValue ?? '')} options={{ @@ -96,9 +96,15 @@ export default function Studio() { flex: 1; `} > - - - + + {/* TODO: prévoir de changer la signature de EngineProvider */} + + +

    ) @@ -106,32 +112,23 @@ export default function Studio() { type ResultsProps = { targets: string[] + rules: string onClickShare: React.MouseEventHandler } -export const Results = ({ targets, onClickShare }: ResultsProps) => { +export const Results = ({ targets, onClickShare, rules }: ResultsProps) => { const [rule, setCurrentTarget] = useState() const currentTarget = rule ?? (last(targets) as string) - const error = Engine.useError() + const engine = useMemo(() => new Engine(rules), [rules]) // EN ATTENDANT d'AVOIR une meilleure gestion d'erreur, on va mocker // console.warn const warnings: string[] = [] const originalWarn = console.warn console.warn = warning => warnings.push(warning) - const analysis = Engine.useEvaluation(currentTarget) + const evaluation = engine.evaluate(currentTarget) console.warn = originalWarn - return error !== null ? ( -
    - {nl2br(error)} -
    - ) : ( + return ( <>
    { {nl2br(warning)}
    ))} - {analysis ? ( + {evaluation ? (

    Résultats

    - {analysis.isApplicable === false ? ( + {evaluation.isApplicable === false ? ( <>{emoji('❌')} Cette règle n'est pas applicable ) : ( <>

    - + {formatValue({ + ...evaluation, + language: 'fr' + })}


    - {analysis.temporalValue - ?.filter(({ value }) => value !== false) - .map(({ start: du, end: au, value }) => ( - - - Du {du} au {au} :{' '} - - {formatValue({ value, unit: analysis.unit })}{' '} -
    -
    - ))} + {'temporalValue' in evaluation && + evaluation.temporalValue && + evaluation.temporalValue + .filter(({ value }) => value !== false) + .map(({ start: du, end: au, value }) => ( + + + Du {du} au {au} :{' '} + + + {formatValue({ + nodeValue: value, + unit: evaluation.unit, + language: 'fr' + })} + {' '} +
    +
    + ))} )}
    @@ -232,13 +240,41 @@ const Layout = styled.div` flex-grow: 1; display: flex; height: 100%; + > :first-child { + width: 55% !important; + } @media (max-width: 960px) { flex-direction: column; padding: 20px; - section { - width: 100%; + > :first-child { + width: 100% !important; } } ` + +class ErrorBoundary extends React.Component { + state: { error: false | string } = { error: false } + + static getDerivedStateFromError(error) { + console.error(error) + return { error: error.message } + } + render() { + if (this.state.error) { + return ( +
    + {nl2br(this.state.error)} +
    + ) + } + return this.props.children + } +} diff --git a/test/contrôles.test.js b/test/contrôles.test.js index 803b112a7..48dcdd6a2 100644 --- a/test/contrôles.test.js +++ b/test/contrôles.test.js @@ -51,7 +51,7 @@ describe('controls', function() { }) it('Should allow imbricated conditions', function() { - const engine = new Engine({ rules: rawRules }) + const engine = new Engine(rawRules) let controls = engine.setSituation({ brut: 2000000 }).controls() expect( controls.find( diff --git a/test/conversation.test.js b/test/conversation.test.js index b92344a78..8363d04eb 100644 --- a/test/conversation.test.js +++ b/test/conversation.test.js @@ -1,102 +1,25 @@ import { expect } from 'chai' -import { assocPath, merge } from 'ramda' -import reducers from 'Reducers/rootReducer' -import rules from 'Rules' -import salariéConfig from '../source/components/simulationConfigs/salarié.yaml' -import { - currentQuestionSelector, - nextStepsSelector -} from '../source/selectors/analyseSelectors' -let baseState = { - simulation: { defaultUnit: '€/an', situation: {}, foldedSteps: [] } -} +import { getNextQuestions } from '../source/components/utils/useNextQuestion' +import Engine from 'Engine' describe('conversation', function() { it('should start with the first missing variable', function() { - let rules = { - // TODO - this won't work without the indirection, figure out why - 'top . startHere': { formule: { somme: ['a', 'b'] } }, - 'top . a': { formule: 'aa' }, - 'top . b': { formule: 'bb' }, - 'top . aa': { question: '?', titre: 'a', unité: '€' }, - 'top . bb': { question: '?', titre: 'b', unité: '€' } - }, - state = merge(baseState, { - rules, - simulation: { - defaultUnit: '€/an', - config: { objectifs: ['top . startHere'] }, - foldedSteps: [] - } - }), - currentQuestion = currentQuestionSelector(state) - - expect(currentQuestion).to.equal('top . aa') - }) - it('should deal with double unfold', function() { - let rules = { + const missingVariables = new Engine({ // TODO - this won't work without the indirection, figure out why - 'top . startHere': { - formule: { somme: ['a', 'b', 'c'] } - }, + 'top . startHere': { formule: { somme: ['a', 'b'] } }, 'top . a': { formule: 'aa' }, 'top . b': { formule: 'bb' }, - 'top . c': { formule: 'cc' }, 'top . aa': { question: '?', titre: 'a', unité: '€' }, - 'top . bb': { question: '?', titre: 'b', unité: '€' }, - 'top . cc': { question: '?', titre: 'c', unité: '€' } - } - - let step1 = merge(baseState, { - rules, - simulation: { - defaultUnit: '€/an', - config: { objectifs: ['top . startHere'] }, - foldedSteps: [] - } - }) - let step2 = reducers( - assocPath(['simulation', 'situation'], { 'top . aa': '1' }, step1), - { - type: 'STEP_ACTION', - name: 'fold', - step: 'top . aa' - } - ) - - let step3 = reducers( - assocPath( - ['simulation', 'situation'], - { 'top . aa': '1', 'top . bb': '1' }, - step2 - ), - { - type: 'STEP_ACTION', - name: 'fold', - step: 'top . bb' - } - ) - let step4 = reducers(step3, { - type: 'STEP_ACTION', - name: 'unfold', - step: 'top . aa' - }) - let lastStep = reducers(step4, { - type: 'STEP_ACTION', - name: 'unfold', - step: 'top . bb' - }) - - expect(currentQuestionSelector(lastStep)).to.equal('top . bb') - expect(lastStep.simulation).to.have.property('foldedSteps') - expect(lastStep.simulation.foldedSteps).to.have.lengthOf(0) + 'top . bb': { question: '?', titre: 'b', unité: '€' } + }).evaluate('top . startHere').missingVariables + expect(getNextQuestions([missingVariables])[0]).to.equal('top . aa') }) - it('should first ask for questions without defaults, then those with defaults', function() { - let rules = { + const engine = new Engine({ net: { formule: 'brut - cotisation' }, brut: { - question: 'Quel est le salaire brut ?' + question: 'Quel est le salaire brut ?', + unité: '€/an' }, cotisation: { formule: { @@ -122,47 +45,24 @@ describe('conversation', function() { question: 'Est-ce un cadre ?', 'par défaut': 'non' } - } - - let step1 = merge(baseState, { - rules, - simulation: { - defaultUnit: '€/an', - config: { objectifs: ['net'] }, - foldedSteps: [] - } }) - expect(currentQuestionSelector(step1)).to.equal('brut') - let step2 = reducers( - assocPath(['simulation', 'situation', 'brut'], '2300', step1), - { - type: 'STEP_ACTION', - name: 'fold', - step: 'brut' - } - ) + expect( + getNextQuestions([engine.evaluate('net').missingVariables])[0] + ).to.equal('brut') - expect(step2.simulation).to.have.property('foldedSteps') - expect(step2.simulation.foldedSteps).to.have.lengthOf(1) - expect(step2.simulation.foldedSteps[0]).to.equal('brut') - expect(currentQuestionSelector(step2)).to.equal('cadre') - }) -}) -describe('real conversation', function() { - it('should not have more than X questions', function(done) { - let state = merge(baseState, { - rules, - simulation: { - defaultUnit: '€/an', - config: salariéConfig, - foldedSteps: [] - } - }), - nextSteps = nextStepsSelector(state) - - expect(nextSteps.length).to.be.below(30) // If this breaks, that's good news - expect(nextSteps.length).to.be.above(10) - done() + engine.setSituation({ + brut: 2300 + }) + + expect( + getNextQuestions([engine.evaluate('net').missingVariables])[0] + ).to.equal(undefined) + + expect( + getNextQuestions([ + engine.evaluate('net', { useDefaultValues: false }).missingVariables + ])[0] + ).to.equal('cadre') }) }) diff --git a/test/ficheDePaieSelector.test.js b/test/ficheDePaieSelector.test.js deleted file mode 100644 index abd718d7d..000000000 --- a/test/ficheDePaieSelector.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import { expect } from 'chai' -import salariéConfig from 'Components/simulationConfigs/salarié.yaml' -import { getRuleFromAnalysis } from 'Engine/ruleUtils' -import rules from 'Rules' -import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' -import { - analysisToCotisationsSelector, - COTISATION_BRANCHE_ORDER -} from 'Selectors/ficheDePaieSelectors' - -let state = { - rules, - simulation: { - defaultUnit: '€/mois', - config: salariéConfig, - situation: { - 'contrat salarié . rémunération . brut de base': '2300', - 'entreprise . effectif': '50' - }, - foldedSteps: [] - } -} - -let cotisations = null, - analysis - -describe('pay slip selector', function() { - beforeEach(() => { - cotisations = analysisToCotisationsSelector(state) - analysis = analysisWithDefaultsSelector(state) - - expect(cotisations).not.to.eq(null) - }) - it('should have cotisations grouped by branches in the proper ordering', function() { - let branches = cotisations.map(([branche]) => branche) - expect(branches).to.eql(COTISATION_BRANCHE_ORDER) - }) - - it('should collect all cotisations in a branche', function() { - let cotisationsSanté = (cotisations.find(([branche]) => - branche.includes('santé') - ) || [])[1].map(cotisation => cotisation.name) - expect(cotisationsSanté).to.have.lengthOf(2) - expect(cotisationsSanté).to.include('maladie') - expect(cotisationsSanté).to.include('complémentaire santé') - }) - - it('should sum all cotisations', function() { - let pat = getRuleFromAnalysis(analysis)( - 'contrat salarié . cotisations . patronales' - ), - sal = getRuleFromAnalysis(analysis)( - 'contrat salarié . cotisations . salariales' - ) - expect(pat.nodeValue).to.be.closeTo(808.9, 5) - expect(sal.nodeValue).to.be.closeTo(498, 5) - }) - - it('should have value for "salarié" and "employeur" for a cotisation', function() { - let cotisationATMP = (cotisations.find(([branche]) => - branche.includes('accidents du travail et maladies professionnelles') - ) || [])[1][0] - expect(cotisationATMP.montant.partSalariale).to.be.closeTo(0, 0.1) - let defaultATMPRate = 2.22 / 100 - expect(cotisationATMP.montant.partPatronale).to.be.closeTo( - 2300 * defaultATMPRate, - 1 - ) - }) -}) diff --git a/test/inversion.test.js b/test/inversion.test.js index 97f851bda..bb6cb3438 100644 --- a/test/inversion.test.js +++ b/test/inversion.test.js @@ -14,7 +14,7 @@ describe('inversions', () => { brut: unité: € ` - const result = new Engine({ rules }) + const result = new Engine(rules) .setSituation({ brut: 2300 }) .evaluate('net') @@ -36,7 +36,7 @@ describe('inversions', () => { avec: - net ` - const result = new Engine({ rules }) + const result = new Engine(rules) .setSituation({ net: 2000 }) .evaluate('brut') @@ -58,9 +58,7 @@ describe('inversions', () => { avec: - net ` - const result = new Engine({ rules }) - .setSituation({ net: 0 }) - .evaluate('brut') + const result = new Engine(rules).setSituation({ net: 0 }).evaluate('brut') expect(result.nodeValue).to.be.closeTo(0, 0.0001) }) @@ -86,10 +84,10 @@ describe('inversions', () => { - net cadre: assiette: - formule: 67 + brut + formule: 67€ + brut ` - const result = new Engine({ rules }).evaluate('brut') + const result = new Engine(rules).evaluate('brut') expect(result.nodeValue).to.be.null expect(Object.keys(result.missingVariables)).to.include('brut') @@ -127,7 +125,7 @@ describe('inversions', () => { taxe: formule: produit: - assiette: 1200 + assiette: 1200 € variations: - si: cadre alors: @@ -135,7 +133,7 @@ describe('inversions', () => { - sinon: taux: 70% ` - const result = new Engine({ rules }) + const result = new Engine(rules) .setSituation({ net: 2000 }) .evaluate('brut') expect(result.nodeValue).to.be.null @@ -176,7 +174,7 @@ describe('inversions', () => { formule: 67 + brut ` - const result = new Engine({ rules }) + const result = new Engine(rules) .setSituation({ net: 2000, cadre: 'oui' }) .evaluate('total') expect(result.nodeValue).to.be.closeTo(3750, 1) @@ -214,7 +212,7 @@ describe('inversions', () => { - net - total ` - const result = new Engine({ rules }) + const result = new Engine(rules) .setSituation({ net: 2000 }) .evaluate('total') expect(result.nodeValue).to.be.closeTo(3750, 1) diff --git a/test/library.test.js b/test/library.test.js index 6e09894ad..67e150b76 100644 --- a/test/library.test.js +++ b/test/library.test.js @@ -7,7 +7,7 @@ import sasuRules from './rules/sasu.yaml' describe('library', function() { it('should evaluate one target with no input data', function() { let target = 'contrat salarié . rémunération . net' - let engine = new Engine({ rules }) + let engine = new Engine(rules) engine.setSituation({ 'contrat salarié . rémunération . brut de base': 2300 }) @@ -23,20 +23,20 @@ ya: yi: formule: yo + 2 ` - let engine = new Engine({ rules }) + let engine = new Engine(rules) expect(engine.evaluate('ya').nodeValue).to.equal(201) expect(engine.evaluate('yi').nodeValue).to.equal(202) }) - it.skip('should let the user add rules to the default ones', function() { - let rules = ` + it.skip('should let the user add rules to an existing rule base', function() { + let extraRules = ` yo: formule: 1 ya: formule: contrat salarié . rémunération . net + yo ` - let engine = new Engine({ extra: rules }) + let engine = new Engine(rules, extraRules) engine.setSituation({ 'contrat salarié . rémunération . brut de base': 2300 }) @@ -47,7 +47,7 @@ ya: 'should let the user extend the rules constellation in a serious manner', function() { let CA = 550 * 16 - let engine = new Engine({ extra: sasuRules }) + let engine = new Engine(rules, sasuRules) engine.setSituation({ 'chiffre affaires': CA }) @@ -64,10 +64,10 @@ ya: 'contrat salarié . rémunération . net après impôt': salaireNetAprèsImpôt, 'chiffre affaires': CA }) - let [revenuDisponible, dividendes] = engine.evaluate([ + let [revenuDisponible, dividendes] = [ 'contrat salarié . rémunération . net après impôt', 'dividendes . net' - ]) + ].map(name => engine.evaluate(name)) expect(revenuDisponible.nodeValue).to.be.closeTo(2324, 1) expect(dividendes.nodeValue).to.be.closeTo(2507, 1) @@ -110,7 +110,7 @@ impôt sur le revenu à payer: plafond: 1177 ` - let engine = new Engine({ rules }) + let engine = new Engine(rules) engine.setSituation({ 'revenu imposable': '48000' }) @@ -119,10 +119,10 @@ impôt sur le revenu à payer: }) it('should let the user define a rule base on a completely different subject', function() { - let engine = new Engine({ rules: co2 }) + let engine = new Engine(co2) engine.setSituation({ 'nombre de douches': 30, - 'chauffage . type': 'gaz', + 'chauffage . type': "'gaz'", 'durée de la douche': 10 }) let value = engine.evaluate('douche . impact') diff --git a/test/mecanisms.test.js b/test/mecanisms.test.js index d7137097a..7001cd85e 100644 --- a/test/mecanisms.test.js +++ b/test/mecanisms.test.js @@ -8,20 +8,21 @@ import { expect } from 'chai' import Engine from 'Engine' import { parseUnit } from '../source/engine/units' +import { coerceArray } from '../source/utils' import testSuites from './load-mecanism-tests' testSuites.forEach(([suiteName, suite]) => { - const engine = new Engine({ rules: suite, useDefaultValues: false }) + const engine = new Engine(suite) describe(`Mécanisme ${suiteName}`, () => { Object.entries(suite) .filter(([, rule]) => rule?.exemples) .forEach(([name, test]) => { const { exemples, 'unité attendue': unit } = test - exemples.forEach( + coerceArray(exemples).forEach( ( { nom: testName, situation, - 'unités par défaut': defaultUnits, + 'unité par défaut': defaultUnit, 'valeur attendue': valeur, 'variables manquantes': expectedMissing }, @@ -37,8 +38,10 @@ testSuites.forEach(([suiteName, suite]) => { () => { const result = engine .setSituation(situation ?? {}) - .setDefaultUnits(defaultUnits) - .evaluate(name) + .evaluate(name, { + unit: defaultUnit, + useDefaultValues: false + }) if (typeof valeur === 'number') { expect(result.nodeValue).to.be.closeTo(valeur, 0.001) } else if (valeur !== undefined) { diff --git a/test/generateQuestions.test.js b/test/missingVariables.test.js similarity index 82% rename from test/generateQuestions.test.js rename to test/missingVariables.test.js index 789aa99d2..70f0e356d 100644 --- a/test/generateQuestions.test.js +++ b/test/missingVariables.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai' import Engine from 'Engine' import rules from 'Rules' -import { getNextSteps } from '../source/engine/generateQuestions' +import { getNextSteps } from '../source/components/utils/useNextQuestion' describe('Missing variables', function() { it('should identify missing variables', function() { @@ -19,8 +19,7 @@ describe('Missing variables', function() { 'sum . evt . ko': {} } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('sum . startHere') - .missingVariables + new Engine(rawRules).evaluate('sum . startHere').missingVariables ) expect(result).to.include('sum . evt . ko') @@ -38,8 +37,7 @@ describe('Missing variables', function() { 'sum . evt . nyet': {} } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('sum . startHere') - .missingVariables + new Engine(rawRules).evaluate('sum . startHere').missingVariables ) expect(result).to.include('sum . evt . nyet') @@ -56,8 +54,7 @@ describe('Missing variables', function() { 'sum . trois': {} } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('sum . startHere') - .missingVariables + new Engine(rawRules).evaluate('sum . startHere').missingVariables ) expect(result).to.be.empty @@ -75,8 +72,7 @@ describe('Missing variables', function() { 'sum . trois': {} } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('sum . startHere') - .missingVariables + new Engine(rawRules).evaluate('sum . startHere').missingVariables ) expect(result).to.be.empty @@ -91,8 +87,7 @@ describe('Missing variables', function() { } } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('top . startHere') - .missingVariables + new Engine(rawRules).evaluate('top . startHere').missingVariables ) expect(result).to.include('top . trois') @@ -108,8 +103,7 @@ describe('Missing variables', function() { } } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('top . startHere') - .missingVariables + new Engine(rawRules).evaluate('top . startHere').missingVariables ) expect(result).to.be.empty @@ -125,8 +119,8 @@ describe('Missing variables', function() { } } const result = Object.keys( - new Engine({ rules: rawRules }) - .setSituation({ 'top . trois': 'ko' }) + new Engine(rawRules) + .setSituation({ 'top . trois': "'ko'" }) .evaluate('top . startHere').missingVariables ) @@ -177,8 +171,7 @@ describe('Missing variables', function() { 'top . quatre': {} } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('top . startHere') - .missingVariables + new Engine(rawRules).evaluate('top . startHere').missingVariables ) expect(result).to.include('top . dix') @@ -204,7 +197,7 @@ describe('nextSteps', function() { } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables + new Engine(rawRules).evaluate('top . sum').missingVariables ) expect(result).to.have.lengthOf(1) @@ -223,7 +216,7 @@ describe('nextSteps', function() { } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables + new Engine(rawRules).evaluate('top . sum').missingVariables ) expect(result).to.have.lengthOf(1) @@ -246,7 +239,7 @@ describe('nextSteps', function() { 'top . sum . evt . ko': {} } const result = Object.keys( - new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables + new Engine(rawRules).evaluate('top . sum').missingVariables ) expect(result).to.eql(['top . sum . evt']) @@ -254,13 +247,15 @@ describe('nextSteps', function() { it('should ask "motif CDD" if "CDD" applies', function() { const result = Object.keys( - new Engine({ rules, useDefaultValues: false }) + new Engine(rules) .setSituation({ 'contrat salarié': 'oui', 'contrat salarié . CDD': 'oui', 'contrat salarié . rémunération . brut de base': '2300' }) - .evaluate('contrat salarié . rémunération . net').missingVariables + .evaluate('contrat salarié . rémunération . net', { + useDefaultValues: false + }).missingVariables ) expect(result).to.include('contrat salarié . CDD . motif') @@ -269,19 +264,19 @@ describe('nextSteps', function() { describe('getNextSteps', function() { it('should give priority to questions that advance most targets', function() { - let missingVariablesByTarget = { - chargé: { + let missingVariablesByTarget = [ + { effectif: 34.01, cadre: 30 }, - net: { + { cadre: 10.1 }, - aides: { + { effectif: 32.0, cadre: 10 } - } + ] let result = getNextSteps(missingVariablesByTarget) @@ -289,17 +284,17 @@ describe('getNextSteps', function() { }) it('should give priority to questions by total weight when advancing the same target count', function() { - let missingVariablesByTarget = { - chargé: { + let missingVariablesByTarget = [ + { effectif: 24.01, cadre: 30 }, - net: { + { effectif: 24.01, cadre: 10.1 }, - aides: {} - } + {} + ] let result = getNextSteps(missingVariablesByTarget) diff --git a/test/mécanismes/allègement.yaml b/test/mécanismes/allègement.yaml index 490bb11ea..8831e12ce 100644 --- a/test/mécanismes/allègement.yaml +++ b/test/mécanismes/allègement.yaml @@ -96,7 +96,7 @@ montant abattu avec plafond numérique: montant: 100000 valeur attendue: 88000 # 85000 s'il n'y avait pas de plafond à la somme abattue -montant franchisé, décote, abattu: +montant franchisé décote abattu: unité: € formule: allègement: diff --git a/test/mécanismes/applicable.yaml b/test/mécanismes/applicable.yaml index 4697071a0..d85620ed8 100644 --- a/test/mécanismes/applicable.yaml +++ b/test/mécanismes/applicable.yaml @@ -1,12 +1,4 @@ statut cadre: - formule: - variations: - - si: 3 > 2 - alors: oui - - sinon: choix du statut cadre - -choix du statut cadre: - par défaut: non prévoyance obligatoire cadre: applicable si: statut cadre @@ -22,4 +14,4 @@ prévoyance obligatoire cadre: - nom: Non Applicabilité situation: statut cadre: non - valeur attendue: 0 + valeur attendue: false diff --git a/test/mécanismes/conversion-unité.yaml b/test/mécanismes/conversion-unité.yaml index 1c9bcdf83..31b2efed5 100644 --- a/test/mécanismes/conversion-unité.yaml +++ b/test/mécanismes/conversion-unité.yaml @@ -18,11 +18,11 @@ Conversion de reference 2: - situation: douches par mois: 30 valeur attendue: 360 - - nom: Unité de variable prioritaire devant les unités par défaut + - nom: unités par défaut prioritaire devant unité de variable situation: douches par mois: 30 - unités par défaut: [douche/mois] - valeur attendue: 360 + unité par défaut: douche/mois + valeur attendue: 30 Conversion de variable: formule: 1.5 kCo2/douche * douches par mois @@ -34,7 +34,7 @@ Conversion de variable: - nom: Unité cible de simulation situation: douches par mois: 20 - unités par défaut: [kCo2/an] + unité par défaut: kCo2/an unité attendue: kCo2/an valeur attendue: 360 @@ -102,7 +102,7 @@ Conversion de mécanisme 2: - situation: assiette annuelle: 36000 valeur attendue: 131.25 - unités par défaut: [€/mois] + unité par défaut: €/mois Conversion dans une expression: unité: €/an @@ -132,7 +132,7 @@ Conversion dans une somme compliquée: exemples: - situation: assiette annuelle: 20000 - unités par défaut: [€/mois] + unité par défaut: €/mois valeur attendue: 130 maladie: @@ -166,11 +166,11 @@ Conversion dans un allègement: assiette: 1000€/an abattement: 10€/mois exemples: - - unités par défaut: [€/an] - valeur attendue: 880 + unité par défaut: €/an + valeur attendue: 880 Conversion dans avec un abattement en %: - unité par défaut: €/an + unité: €/an formule: allègement: assiette: 1000€/an @@ -196,10 +196,10 @@ Conversion avec plusieurs échelons: - prévoyance cadre - 35€/mois exemples: - - unités par défaut: [€/an] - situation: - assiette mensuelle: 1100 - valeur attendue: 600 + unité par défaut: €/an + situation: + assiette mensuelle: 1100 + valeur attendue: 600 Conversion de situation: formule: @@ -207,25 +207,7 @@ Conversion de situation: - retraite - mutuelle exemples: - - unités par défaut: [€/an] - situation: - retraite: 4000 - valeur attendue: 4360 - -rémunération brute: - unité par défaut: €/mois - -Conversion de situation avec unité: - unité: €/an - formule: - produit: - 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 + unité par défaut: €/an + situation: + retraite: 4000 + valeur attendue: 4360 diff --git a/test/mécanismes/date.yaml b/test/mécanismes/date.yaml index 8b7710aa8..beef4a793 100644 --- a/test/mécanismes/date.yaml +++ b/test/mécanismes/date.yaml @@ -34,4 +34,4 @@ Applicable si: valeur attendue: 10 - situation: date de création: 09/02/2019 - valeur attendue: 0 + valeur attendue: false diff --git a/test/mécanismes/expressions.yaml b/test/mécanismes/expressions.yaml index 586462bd1..90612b300 100644 --- a/test/mécanismes/expressions.yaml +++ b/test/mécanismes/expressions.yaml @@ -172,7 +172,8 @@ négation: pourcentage: formule: 38.1% exemples: - - valeur attendue: 0.381 + - valeur attendue: 38.1 + unité attendue: '%' #- test: variable modifiée temporellement multiplication et pourcentage: @@ -213,10 +214,10 @@ test de possibilités: formule: catégorie d'activité = 'artisanale' exemples: - situation: - catégorie d'activité: artisanale + catégorie d'activité: "'artisanale'" valeur attendue: true - situation: - catégorie d'activité: commerciale + catégorie d'activité: "'commerciale'" valeur attendue: false revenu: @@ -267,6 +268,24 @@ variables négatives dans expression: - situation: salaire de base: 3000 valeur attendue: -300 + +expression dans situation: + formule: 10% * salaire de base + exemples: + - situation: + salaire de base: 12 * 100 + unité attendue: $ + valeur attendue: 120 + +salaire: + unité: €/mois +expression dans situation 2: + formule: 10% * salaire + exemples: + - situation: + salaire: 48k€/an + unité attendue: €/mois + valeur attendue: 400 # TODO # expression sur plusieurs lignes: # formule: > diff --git a/test/mécanismes/le-maximum-de.yaml b/test/mécanismes/le-maximum-de.yaml index 09fe34574..2a287044e 100644 --- a/test/mécanismes/le-maximum-de.yaml +++ b/test/mécanismes/le-maximum-de.yaml @@ -9,9 +9,7 @@ Maximum: taux: 9% exemples: - - nom: - situation: - valeur attendue: 1 + - valeur attendue: 1 a: applicable si: non diff --git a/test/mécanismes/question-conditionelle.yaml b/test/mécanismes/question-conditionelle.yaml index fbc3ec391..6dc0596ec 100644 --- a/test/mécanismes/question-conditionelle.yaml +++ b/test/mécanismes/question-conditionelle.yaml @@ -3,7 +3,7 @@ enfants: nombre enfants: applicable si: enfants question: Combien d'enfants avez vous ? - par défaut: 4 + par défaut: 4 enfants famille nombreuse: titre: question conditionnelle diff --git a/test/mécanismes/remplace.yaml b/test/mécanismes/remplace.yaml index b1fe273c0..16a405534 100644 --- a/test/mécanismes/remplace.yaml +++ b/test/mécanismes/remplace.yaml @@ -177,7 +177,7 @@ x . y: remplace: z formule: 20 -remplacement non applicable (branche desactivée): +remplacement non applicable car branche desactivée: formule: z exemples: - valeur attendue: 1 diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml index 3b47d7679..0114a26ed 100644 --- a/test/mécanismes/régularisation.yaml +++ b/test/mécanismes/régularisation.yaml @@ -88,4 +88,9 @@ régularisation . test variations 1: régularisation . test variations 2: formule: cotisation spéciale | du 01/02/2020 | au 29/02/2020 exemples: - - valeur attendue: 660 + - valeur attendue: 0 + +régularisation . test variations 3: + formule: cotisation spéciale | du 01/03/2020 | au 31/03/2020 + exemples: + - valeur attendue: 1380 diff --git a/test/mécanismes/toutes-ces-conditions.yaml b/test/mécanismes/toutes-ces-conditions.yaml index 7ec5510e1..ae07b7682 100644 --- a/test/mécanismes/toutes-ces-conditions.yaml +++ b/test/mécanismes/toutes-ces-conditions.yaml @@ -21,7 +21,7 @@ remboursement dépot de garantie: situation: dégradation mineure: oui dégradation majeure: oui - valeur attendue: 0 + valeur attendue: false variables manquantes: [] - nom: C situation: diff --git a/test/mécanismes/une-de-ces-conditions.yaml b/test/mécanismes/une-de-ces-conditions.yaml index e651c38d0..af37bb257 100644 --- a/test/mécanismes/une-de-ces-conditions.yaml +++ b/test/mécanismes/une-de-ces-conditions.yaml @@ -14,7 +14,7 @@ remboursement dépot de garantie: - nom: Est vraie -> non applicable -> 0 situation: dégradation mineure: oui - valeur attendue: 0 + valeur attendue: false variables manquantes: [] - nom: Est fausse -> en attente de l'autre situation: diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml index 0c8d201ec..e8519bf34 100644 --- a/test/mécanismes/variable-temporelle.yaml +++ b/test/mécanismes/variable-temporelle.yaml @@ -85,7 +85,7 @@ variable temporelle numérique . test addition: valeur attendue: 30 prix avec variations: - formule: prix * (50% | du 01/01/2020 | au 31/01/2020) + formule: prix - (prix * 50% | du 01/01/2020 | au 31/01/2020) début: fin: variable temporelle numérique . expression . multiplication: @@ -96,14 +96,17 @@ variable temporelle numérique . expression . multiplication: début: 01/01/2020 fin: 31/01/2020 valeur attendue: 10 + unité attendue: €/mois - situation: début: 01/01/2020 fin: 29/02/2020 valeur attendue: 20 + unité attendue: €/mois - situation: début: 01/02/2020 fin: 31/03/2020 valeur attendue: 30 + unité attendue: €/mois taux associé: formule: @@ -112,7 +115,7 @@ taux associé: alors: 10%/mois - si: prix avec variations < 20 €/mois alors: 60%/mois - # Cette formule peut paraître bizarre, mais lorsque multiplication est non + # Cette formule peut paraître bizarre, mais lorsque le prix est non # applicable, c'est bien le sinon qui s'applique - sinon: 5%/mois variable temporelle numérique . variation: diff --git a/test/mécanismes/variations.yaml b/test/mécanismes/variations.yaml index 930cb8c38..289e69206 100644 --- a/test/mécanismes/variations.yaml +++ b/test/mécanismes/variations.yaml @@ -80,10 +80,10 @@ variations avec cas défaut calculé: variables manquantes: [] effectif: - unité: _ + unité: '' effectif plafond: - unité: _ + unité: '' plusieurs variations et un cas défaut: formule: @@ -100,7 +100,7 @@ plusieurs variations et un cas défaut: - nom: 1er cas situation: effectif: 300 - valeur attendue: 0.05 + valeur attendue: 5 - nom: 2ème cas, non résolu situation: effectif: 40 @@ -111,7 +111,7 @@ plusieurs variations et un cas défaut: situation: effectif: 20 effectif plafond: 60 - valeur attendue: 0.01 + valeur attendue: 1 variations au sein d'un mécanisme: formule: diff --git a/test/real-rules.test.js b/test/real-rules.test.js index b1b2717dd..fdb0460f9 100644 --- a/test/real-rules.test.js +++ b/test/real-rules.test.js @@ -6,7 +6,7 @@ import rules from 'Rules' // les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle, // comme dans sa formule let parsedRules = parseRules(rules) -const engine = new Engine({ rules: parsedRules }) +const engine = new Engine(parsedRules) let runExamples = (examples, rule) => examples.map(ex => { const expected = ex['valeur attendue'] @@ -19,10 +19,9 @@ let runExamples = (examples, rule) => ) const evaluation = engine .setSituation(situation) - .setDefaultUnits( - ex['unités par défaut'] ?? [rule['unité par défaut'] ?? '€/mois'] - ) - .evaluate(rule.dottedName) + .evaluate(rule.dottedName, { + unit: ex['unités par défaut']?.[0] ?? rule['unité par défaut'] + }) const ok = evaluation.nodeValue === expected ? true diff --git a/test/regressions/__snapshots__/simulations.jest.js.snap b/test/regressions/__snapshots__/simulations.jest.js.snap index 20d4f55f8..269dc387e 100644 --- a/test/regressions/__snapshots__/simulations.jest.js.snap +++ b/test/regressions/__snapshots__/simulations.jest.js.snap @@ -1,469 +1,469 @@ // 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`] = `"[1863]"`; -exports[`calculate simulations-artiste-auteur: bnc 3`] = `"[932]"`; +exports[`calculate simulations-artiste-auteur: bnc 3`] = `"[931]"`; -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`] = `"[12410]"`; +exports[`calculate simulations-artiste-auteur: salarié 3`] = `"[12410]"`; -exports[`calculate simulations-auto-entrepreneur: ACRE 1`] = `"[21394,1394,20000,0,20000]"`; +exports[`calculate simulations-auto-entrepreneur: ACRE 1`] = `"[21394,116,20000,0,20000]"`; -exports[`calculate simulations-auto-entrepreneur: ACRE 2`] = `"[31029,1029,30000,0,30000]"`; +exports[`calculate simulations-auto-entrepreneur: ACRE 2`] = `"[31029,86,30000,0,30000]"`; -exports[`calculate simulations-auto-entrepreneur: ACRE 3`] = `"[44304,4304,40000,0,40000]"`; +exports[`calculate simulations-auto-entrepreneur: ACRE 3`] = `"[44304,359,40000,0,40000]"`; -exports[`calculate simulations-auto-entrepreneur: aides 1`] = `"[5348,348,5000,0,5000]"`; +exports[`calculate simulations-auto-entrepreneur: aides 1`] = `"[5348,29,5000,0,5000]"`; -exports[`calculate simulations-auto-entrepreneur: aides 2`] = `"[53485,3485,50000,93,49907]"`; +exports[`calculate simulations-auto-entrepreneur: aides 2`] = `"[53485,290,50000,93,49907]"`; -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,591,25000,706,24294]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 1`] = `"[574,74,500,0,500]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 1`] = `"[574,6,500,0,500]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 2`] = `"[1148,148,1000,0,1000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 2`] = `"[1148,12,1000,0,1000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 3`] = `"[2297,297,2000,0,2000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 3`] = `"[2297,25,2000,0,2000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 4`] = `"[5742,742,5000,0,5000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 4`] = `"[5742,62,5000,0,5000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 5`] = `"[11483,1483,10000,0,10000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 5`] = `"[11483,124,10000,0,10000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 6`] = `"[22966,2966,20000,0,20000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 6`] = `"[22966,247,20000,0,20000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 7`] = `"[57415,7415,50000,275,49725]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 7`] = `"[57415,618,50000,275,49725]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 8`] = `"[80381,10381,70000,1340,68660]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 8`] = `"[80381,865,70000,1340,68660]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 9`] = `"[114830,14830,100000,4008,95992]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 9`] = `"[114830,1236,100000,4008,95992]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 10`] = `"[1148303,148303,1000000,131979,868021]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 10`] = `"[1148303,12359,1000000,131979,868021]"`; -exports[`calculate simulations-indépendant: acre 1`] = `"[73024,23024,50000,51980,8052,41948,null,73024]"`; +exports[`calculate simulations-indépendant: acre 1`] = `"[73024,23024,50000,51980,8052,41948,0,73024]"`; -exports[`calculate simulations-indépendant: activité 1`] = `"[28923,8923,20000,20783,604,19396,null,28923]"`; +exports[`calculate simulations-indépendant: activité 1`] = `"[28923,8923,20000,20783,604,19396,0,28923]"`; -exports[`calculate simulations-indépendant: activité 2`] = `"[29101,9101,20000,20787,604,19396,null,29101]"`; +exports[`calculate simulations-indépendant: activité 2`] = `"[29101,9101,20000,20787,604,19396,0,29101]"`; -exports[`calculate simulations-indépendant: cotisations minimales 1`] = `"[1373,1273,100,134,0,100,null,1373]"`; +exports[`calculate simulations-indépendant: cotisations minimales 1`] = `"[1373,1273,100,134,0,100,0,1373]"`; -exports[`calculate simulations-indépendant: cotisations minimales 2`] = `"[245,145,100,104,0,100,null,245]"`; +exports[`calculate simulations-indépendant: cotisations minimales 2`] = `"[245,145,100,104,0,100,0,245]"`; -exports[`calculate simulations-indépendant: impôt sur le revenu 1`] = `"[29085,9085,20000,20787,603,19397,null,29085]"`; +exports[`calculate simulations-indépendant: impôt sur le revenu 1`] = `"[29085,9085,20000,20787,603,19397,0,29085]"`; -exports[`calculate simulations-indépendant: impôt sur le revenu 2`] = `"[73024,23024,50000,51980,8213,41787,null,73024]"`; +exports[`calculate simulations-indépendant: impôt sur le revenu 2`] = `"[73024,23024,50000,51980,8213,41787,0,73024]"`; -exports[`calculate simulations-indépendant: impôt sur le revenu 3`] = `"[29085,9085,20000,20787,2079,17921,null,29085]"`; +exports[`calculate simulations-indépendant: impôt sur le revenu 3`] = `"[29085,9085,20000,20787,2079,17921,0,29085]"`; -exports[`calculate simulations-indépendant: inversions 1`] = `"[2000,1384,616,667,0,616,null,2000]"`; +exports[`calculate simulations-indépendant: inversions 1`] = `"[2000,1384,616,667,0,616,0,2000]"`; -exports[`calculate simulations-indépendant: inversions 2`] = `"[50000,16003,33997,35352,3563,30434,null,50000]"`; +exports[`calculate simulations-indépendant: inversions 2`] = `"[50000,16003,33997,35352,3563,30434,0,50000]"`; -exports[`calculate simulations-indépendant: inversions 3`] = `"[14596,4596,10000,10394,0,10000,null,14596]"`; +exports[`calculate simulations-indépendant: inversions 3`] = `"[14596,4596,10000,10394,0,10000,0,14596]"`; -exports[`calculate simulations-indépendant: inversions 4`] = `"[88547,27360,61187,63588,11187,50000,null,88547]"`; +exports[`calculate simulations-indépendant: inversions 4`] = `"[88547,27360,61187,63588,11187,50000,0,88547]"`; -exports[`calculate simulations-indépendant: inversions 5`] = `"[14596,4596,10000,10394,0,10000,null,15596]"`; +exports[`calculate simulations-indépendant: inversions 5`] = `"[14596,4596,10000,10394,0,10000,1000,15596]"`; -exports[`calculate simulations-indépendant: inversions 6`] = `"[19000,5928,13072,13585,0,13072,1000,20000]"`; +exports[`calculate simulations-indépendant: inversions 6`] = `"[19000,5928,13072,13585,0,13072,1000,20000]"`; -exports[`calculate simulations-indépendant: inversions 7`] = `"[18000,5626,12374,12860,0,12374,2000,20000]"`; +exports[`calculate simulations-indépendant: inversions 7`] = `"[18000,5626,12374,12860,0,12374,2000,20000]"`; -exports[`calculate simulations-indépendant: échelle de revenus 1`] = `"[1859,1359,500,548,0,500,null,1859]"`; +exports[`calculate simulations-indépendant: échelle de revenus 1`] = `"[1859,1359,500,548,0,500,0,1859]"`; -exports[`calculate simulations-indépendant: échelle de revenus 2`] = `"[2467,1467,1000,1064,0,1000,null,2467]"`; +exports[`calculate simulations-indépendant: échelle de revenus 2`] = `"[2467,1467,1000,1064,0,1000,0,2467]"`; -exports[`calculate simulations-indépendant: échelle de revenus 3`] = `"[3075,1575,1500,1581,0,1500,null,3075]"`; +exports[`calculate simulations-indépendant: échelle de revenus 3`] = `"[3075,1575,1500,1581,0,1500,0,3075]"`; -exports[`calculate simulations-indépendant: échelle de revenus 4`] = `"[3682,1682,2000,2097,0,2000,null,3682]"`; +exports[`calculate simulations-indépendant: échelle de revenus 4`] = `"[3682,1682,2000,2097,0,2000,0,3682]"`; -exports[`calculate simulations-indépendant: échelle de revenus 5`] = `"[7427,2427,5000,5199,0,5000,null,7427]"`; +exports[`calculate simulations-indépendant: échelle de revenus 5`] = `"[7427,2427,5000,5199,0,5000,0,7427]"`; -exports[`calculate simulations-indépendant: échelle de revenus 6`] = `"[14596,4596,10000,10394,0,10000,null,14596]"`; +exports[`calculate simulations-indépendant: échelle de revenus 6`] = `"[14596,4596,10000,10394,0,10000,0,14596]"`; -exports[`calculate simulations-indépendant: échelle de revenus 7`] = `"[139594,39594,100000,103788,24245,75755,null,139594]"`; +exports[`calculate simulations-indépendant: échelle de revenus 7`] = `"[139594,39594,100000,103788,24245,75755,0,139594]"`; -exports[`calculate simulations-indépendant: échelle de revenus 8`] = `"[1239955,239955,1000000,1033666,467505,532495,null,1239955]"`; +exports[`calculate simulations-indépendant: échelle de revenus 8`] = `"[1239955,239955,1000000,1033666,467505,532495,0,1239955]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - ACRE 1`] = `"[7257,7257,7184,4,13,16]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): ACRE 1`] = `"[605,0,0,7184,4,13]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - ACRE 2`] = `"[14963,14963,14544,4,26,32]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): ACRE 2`] = `"[1247,0,0,14544,4,26]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - ACRE 3`] = `"[21575,22669,21905,4,40,48]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): ACRE 3`] = `"[1889,0,0,21905,4,40]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - Contrats Madelin 1`] = `"[16490,16673,16178,4,29,35]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): Contrats Madelin 1`] = `"[1389,0,0,16178,4,29]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - Contrats Madelin 2`] = `"[16490,16673,16178,4,29,35]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): Contrats Madelin 2`] = `"[1389,0,0,16178,4,29]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - Contrats Madelin 3`] = `"[16490,16673,16178,4,29,35]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): Contrats Madelin 3`] = `"[1389,0,0,16178,4,29]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - Contrats Madelin 4`] = `"[10982,10982,10742,4,19,23]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): Contrats Madelin 4`] = `"[915,0,0,10742,4,19]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - Contrats Madelin 5`] = `"[118288,173630,154362,4,46,203]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): Contrats Madelin 5`] = `"[14469,0,0,154362,4,46]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - Contrats Madelin 6`] = `"[10982,10982,10742,4,19,23]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): Contrats Madelin 6`] = `"[915,0,0,10742,4,19]"`; -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é): activités 1`] = `"[915,0,0,10742,4,19]"`; -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é): activités 2`] = `"[915,0,0,10742,4,19]"`; -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é): activités 3`] = `"[915,0,0,10742,4,19]"`; -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é): activités 4`] = `"[915,0,0,10742,4,19]"`; -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 5`] = `"[915,0,0,10742,4,19]"`; -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é): avec charges 1`] = `"[441,0,0,5306,4,10]"`; -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é): avec charges 2`] = `"[915,0,0,10742,4,19]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 1`] = `"[-2313,-2313,2488,0,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): échelle de rémunération 1`] = `"[0,0,0,0,0,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 2`] = `"[169,169,139,0,1,1]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): échelle de rémunération 2`] = `"[14,0,0,139,0,1]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 3`] = `"[738,738,323,0,2,2]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): échelle de rémunération 3`] = `"[62,0,0,323,0,2]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 4`] = `"[2446,2446,2588,2,5,6]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): échelle de rémunération 4`] = `"[204,0,0,2588,2,5]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 5`] = `"[5291,5291,5306,4,10,12]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): échelle de rémunération 5`] = `"[441,0,0,5306,4,10]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 6`] = `"[10982,10982,10742,4,19,23]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): échelle de rémunération 6`] = `"[915,0,0,10742,4,19]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 7`] = `"[25971,28055,27050,4,46,59]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): échelle de rémunération 7`] = `"[2338,0,0,27050,4,46]"`; -exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - échelle de rémunération 8`] = `"[46812,57017,52684,4,46,119]"`; +exports[`calculate simulations-rémunération-dirigeant (assimilé salarié): échelle de rémunération 8`] = `"[4751,0,0,52684,4,46]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - ACRE 1`] = `"[9349,9349,2046,2,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): ACRE 1`] = `"[0,0,779,2046,2,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - ACRE 2`] = `"[18697,18697,4093,3,8,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): ACRE 2`] = `"[0,0,1558,4093,3,8]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - ACRE 3`] = `"[28046,28046,6139,4,12,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): ACRE 3`] = `"[0,0,2337,6139,4,12]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - Contrats Madelin 1`] = `"[24834,24834,8186,4,16,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): Contrats Madelin 1`] = `"[0,0,2070,8186,4,16]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - Contrats Madelin 2`] = `"[24834,24834,8186,4,16,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): Contrats Madelin 2`] = `"[0,0,2070,8186,4,16]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - Contrats Madelin 3`] = `"[24834,24834,8186,4,16,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): Contrats Madelin 3`] = `"[0,0,2070,8186,4,16]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - Contrats Madelin 4`] = `"[17288,17288,4298,4,8,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): Contrats Madelin 4`] = `"[0,0,1441,4298,4,8]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - Contrats Madelin 5`] = `"[235917,259318,39357,4,56,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): Contrats Madelin 5`] = `"[0,0,21610,39357,4,56]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - Contrats Madelin 6`] = `"[17352,17352,4195,3,8,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): Contrats Madelin 6`] = `"[0,0,1446,4195,3,8]"`; -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): activités 1`] = `"[0,0,1298,6600,4,18]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 2`] = `"[15560,15560,0,4,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): activités 2`] = `"[0,0,1297,0,4,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - activités 3`] = `"[17336,17336,4093,3,8,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): activités 3`] = `"[0,0,1445,4093,3,8]"`; -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): activités 4`] = `"[0,0,1451,4093,3,8]"`; -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): activités 5`] = `"[0,0,1451,4093,3,8]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - avec charges 1`] = `"[8450,8450,2456,3,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): avec charges 1`] = `"[0,0,704,2456,3,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - avec charges 2`] = `"[15480,15480,7163,4,14,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): avec charges 2`] = `"[0,0,1290,7163,4,14]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 1`] = `"[87,87,6,0,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): échelle de rémunération 1`] = `"[0,0,7,6,0,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 2`] = `"[871,871,60,0,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): échelle de rémunération 2`] = `"[0,0,73,60,0,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 3`] = `"[1742,1742,119,0,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): échelle de rémunération 3`] = `"[0,0,145,119,0,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 4`] = `"[4354,4354,1023,1,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): échelle de rémunération 4`] = `"[0,0,363,1023,1,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 5`] = `"[8709,8709,2046,2,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): échelle de rémunération 5`] = `"[0,0,726,2046,2,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 6`] = `"[17417,17417,4093,3,8,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): échelle de rémunération 6`] = `"[0,0,1451,4093,3,8]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 7`] = `"[43543,43543,10232,4,20,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): échelle de rémunération 7`] = `"[0,0,3629,10232,4,20]"`; -exports[`calculate simulations-rémunération-dirigeant: Auto-entrepreneur - échelle de rémunération 8`] = `"[84367,87085,20465,4,40,0]"`; +exports[`calculate simulations-rémunération-dirigeant (auto-entrepreneur): échelle de rémunération 8`] = `"[0,0,7257,20465,4,40]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - ACRE 1`] = `"[8215,8215,6018,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): ACRE 1`] = `"[0,8215,0,6018,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - ACRE 2`] = `"[16442,16527,12103,4,24,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): ACRE 2`] = `"[0,16527,0,12103,4,24]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - ACRE 3`] = `"[20731,21570,15799,4,31,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): ACRE 3`] = `"[0,21570,0,15799,4,31]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - Contrats Madelin 1`] = `"[19279,20567,15183,4,30,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): Contrats Madelin 1`] = `"[0,20567,0,15183,4,30]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - Contrats Madelin 2`] = `"[18737,20264,15648,4,30,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): Contrats Madelin 2`] = `"[0,20264,0,15648,4,30]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - Contrats Madelin 3`] = `"[19779,20620,15102,4,29,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): Contrats Madelin 3`] = `"[0,20620,0,15102,4,29]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - Contrats Madelin 4`] = `"[13769,13769,10084,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): Contrats Madelin 4`] = `"[0,13769,0,10084,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - Contrats Madelin 5`] = `"[144184,226878,57937,4,56,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): Contrats Madelin 5`] = `"[0,226878,0,57937,4,56]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - Contrats Madelin 6`] = `"[13769,13769,10084,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): Contrats Madelin 6`] = `"[0,13769,0,10084,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 1`] = `"[13886,13886,10166,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): activités 1`] = `"[0,13886,0,10166,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 2`] = `"[14645,14645,0,4,0,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): activités 2`] = `"[0,14645,0,0,4,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 3`] = `"[13758,13758,10075,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): activités 3`] = `"[0,13758,0,10075,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 4`] = `"[13769,13769,10084,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): activités 4`] = `"[0,13769,0,10084,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - activités 5`] = `"[13769,13769,10084,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): activités 5`] = `"[0,13769,0,10084,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - avec charges 1`] = `"[6795,6795,4977,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): avec charges 1`] = `"[0,6795,0,4977,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - avec charges 2`] = `"[13769,13769,10084,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): avec charges 2`] = `"[0,13769,0,10084,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 1`] = `"[-1044,-1044,0,3,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): échelle de rémunération 1`] = `"[0,-1044,0,0,3,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 2`] = `"[-225,-225,0,3,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): échelle de rémunération 2`] = `"[0,-225,0,0,3,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 3`] = `"[616,616,470,3,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): échelle de rémunération 3`] = `"[0,616,0,470,3,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 4`] = `"[3084,3084,2267,3,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): échelle de rémunération 4`] = `"[0,3084,0,2267,3,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 5`] = `"[6795,6795,4977,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): échelle de rémunération 5`] = `"[0,6795,0,4977,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 6`] = `"[13769,13769,10084,4,21,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): échelle de rémunération 6`] = `"[0,13769,0,10084,4,21]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 7`] = `"[30434,33997,24912,4,48,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): échelle de rémunération 7`] = `"[0,33997,0,24912,4,48]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 8`] = `"[56273,69895,36431,4,56,0]"`; +exports[`calculate simulations-rémunération-dirigeant (indépendant): échelle de rémunération 8`] = `"[0,69895,0,36431,4,56]"`; -exports[`calculate simulations-salarié: JEI 1`] = `"[3440,0,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: JEI 1`] = `"[3440,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: JEI 2`] = `"[26635,0,0,20000,15969,10681]"`; +exports[`calculate simulations-salarié: JEI 2`] = `"[26635,0,20000,15969,10681]"`; -exports[`calculate simulations-salarié: JEI 3`] = `"[4517,0,0,4000,3141,2741]"`; +exports[`calculate simulations-salarié: JEI 3`] = `"[4517,0,4000,3141,2741]"`; -exports[`calculate simulations-salarié: activité partielle 1`] = `"[27,1218,0,1560,1197,1197]"`; +exports[`calculate simulations-salarié: activité partielle 1`] = `"[27,0,1560,1197,1197]"`; -exports[`calculate simulations-salarié: activité partielle 2`] = `"[27,2800,0,4000,2594,2365]"`; +exports[`calculate simulations-salarié: activité partielle 2`] = `"[27,0,4000,2594,2365]"`; -exports[`calculate simulations-salarié: activité partielle 3`] = `"[778,4849,0,8000,5209,4253]"`; +exports[`calculate simulations-salarié: activité partielle 3`] = `"[778,0,8000,5209,4253]"`; -exports[`calculate simulations-salarié: activité partielle 4`] = `"[852,2240,0,4000,2704,2444]"`; +exports[`calculate simulations-salarié: activité partielle 4`] = `"[852,0,4000,2704,2444]"`; -exports[`calculate simulations-salarié: activité partielle 5`] = `"[2483,1400,0,4000,2870,2562]"`; +exports[`calculate simulations-salarié: activité partielle 5`] = `"[2483,0,4000,2870,2562]"`; -exports[`calculate simulations-salarié: activité partielle 6`] = `"[27,2100,3750,3000,1940,1848]"`; +exports[`calculate simulations-salarié: activité partielle 6`] = `"[27,3750,3000,1940,1848]"`; -exports[`calculate simulations-salarié: activité partielle 7`] = `"[27,2800,0,4000,2594,2497]"`; +exports[`calculate simulations-salarié: activité partielle 7`] = `"[27,0,4000,2594,2497]"`; -exports[`calculate simulations-salarié: activité partielle 8`] = `"[227,1400,0,2000,1578,1547]"`; +exports[`calculate simulations-salarié: activité partielle 8`] = `"[227,0,2000,1578,1547]"`; -exports[`calculate simulations-salarié: activité partielle 9`] = `"[1156,700,0,2000,1540,1510]"`; +exports[`calculate simulations-salarié: activité partielle 9`] = `"[1156,0,2000,1540,1510]"`; -exports[`calculate simulations-salarié: activité partielle 10`] = `"[327,4200,0,6000,4182,3511]"`; +exports[`calculate simulations-salarié: activité partielle 10`] = `"[327,0,6000,4182,3511]"`; -exports[`calculate simulations-salarié: aides 1`] = `"[2302,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: aides 1`] = `"[2302,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: aides 2`] = `"[12823,0,0,10000,8911,7667]"`; +exports[`calculate simulations-salarié: aides 2`] = `"[12823,0,10000,8911,7667]"`; -exports[`calculate simulations-salarié: aides 3`] = `"[2062,417,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: aides 3`] = `"[2062,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: aides 4`] = `"[2292,208,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: aides 4`] = `"[2292,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: apprentissage 1`] = `"[1552,0,0,1500,1448,1448]"`; +exports[`calculate simulations-salarié: apprentissage 1`] = `"[1552,0,1500,1448,1448]"`; -exports[`calculate simulations-salarié: apprentissage 2`] = `"[1385,167,0,1500,1448,1448]"`; +exports[`calculate simulations-salarié: apprentissage 2`] = `"[1385,0,1500,1448,1448]"`; -exports[`calculate simulations-salarié: assimilé salarié 1`] = `"[7015,0,0,5000,3943,3318]"`; +exports[`calculate simulations-salarié: assimilé salarié 1`] = `"[7015,0,5000,3943,3318]"`; -exports[`calculate simulations-salarié: assimilé salarié 2`] = `"[1583,0,0,1500,1163,1163]"`; +exports[`calculate simulations-salarié: assimilé salarié 2`] = `"[1583,0,1500,1163,1163]"`; -exports[`calculate simulations-salarié: assimilé salarié 3`] = `"[3685,0,0,3000,2348,2172]"`; +exports[`calculate simulations-salarié: assimilé salarié 3`] = `"[3685,0,3000,2348,2172]"`; -exports[`calculate simulations-salarié: atmp 1`] = `"[2534,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: atmp 1`] = `"[2534,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: avantages 1`] = `"[2667,0,0,2000,1540,1492]"`; +exports[`calculate simulations-salarié: avantages 1`] = `"[2667,0,2000,1540,1492]"`; -exports[`calculate simulations-salarié: avantages 2`] = `"[2677,0,0,2000,1539,1490]"`; +exports[`calculate simulations-salarié: avantages 2`] = `"[2677,0,2000,1539,1490]"`; -exports[`calculate simulations-salarié: avantages 3`] = `"[2587,0,0,2000,1549,1506]"`; +exports[`calculate simulations-salarié: avantages 3`] = `"[2587,0,2000,1549,1506]"`; -exports[`calculate simulations-salarié: cadre 1`] = `"[4122,0,0,3000,2348,2171]"`; +exports[`calculate simulations-salarié: cadre 1`] = `"[4122,0,3000,2348,2171]"`; -exports[`calculate simulations-salarié: cdd 1`] = `"[2509,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: cdd 1`] = `"[2509,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: cdd 2`] = `"[2591,0,0,2000,1599,1557]"`; +exports[`calculate simulations-salarié: cdd 2`] = `"[2591,0,2000,1599,1557]"`; -exports[`calculate simulations-salarié: cdd 3`] = `"[3394,0,0,2400,1967,1883]"`; +exports[`calculate simulations-salarié: cdd 3`] = `"[3394,0,2400,1967,1883]"`; -exports[`calculate simulations-salarié: effectif 1`] = `"[2479,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: effectif 1`] = `"[2479,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: effectif 2`] = `"[2525,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: effectif 2`] = `"[2525,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: effectif 3`] = `"[2539,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: effectif 3`] = `"[2539,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: effectif 4`] = `"[2539,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: effectif 4`] = `"[2539,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: frais pro - DFS 1`] = `"[2242,0,0,2000,1630,1630]"`; +exports[`calculate simulations-salarié: frais pro - DFS 1`] = `"[2242,0,2000,1630,1630]"`; -exports[`calculate simulations-salarié: frais pro - DFS 2`] = `"[2335,0,0,2000,1584,1544]"`; +exports[`calculate simulations-salarié: frais pro - DFS 2`] = `"[2335,0,2000,1584,1544]"`; -exports[`calculate simulations-salarié: frais pro - DFS 3`] = `"[2265,0,0,2000,1606,1563]"`; +exports[`calculate simulations-salarié: frais pro - DFS 3`] = `"[2265,0,2000,1606,1563]"`; -exports[`calculate simulations-salarié: frais pro - DFS 4`] = `"[2243,0,0,2000,1613,1567]"`; +exports[`calculate simulations-salarié: frais pro - DFS 4`] = `"[2243,0,0,2000,1613,1569]"`; -exports[`calculate simulations-salarié: frais pro - DFS 5`] = `"[2437,0,0,2000,1590,1590]"`; +exports[`calculate simulations-salarié: frais pro - DFS 5`] = `"[2437,0,2000,1590,1590]"`; -exports[`calculate simulations-salarié: frais pro - DFS 6`] = `"[1767,0,0,1700,1364,1364]"`; +exports[`calculate simulations-salarié: frais pro - DFS 6`] = `"[1767,0,1700,1364,1364]"`; -exports[`calculate simulations-salarié: frais pro - DFS 7`] = `"[3287,0,0,2600,2125,2097]"`; +exports[`calculate simulations-salarié: frais pro - DFS 7`] = `"[3287,0,2600,2125,2097]"`; -exports[`calculate simulations-salarié: frais pro - IKV 1`] = `"[4367,0,0,3200,2530,2320]"`; +exports[`calculate simulations-salarié: frais pro - IKV 1`] = `"[4367,0,3200,2530,2320]"`; -exports[`calculate simulations-salarié: frais pro - IKV 2`] = `"[4346,0,0,3200,2511,2302]"`; +exports[`calculate simulations-salarié: frais pro - IKV 2`] = `"[4346,0,3200,2511,2302]"`; -exports[`calculate simulations-salarié: frais pro - IKV 3`] = `"[2774,0,0,2157,1685,1630]"`; +exports[`calculate simulations-salarié: frais pro - IKV 3`] = `"[2774,0,2157,1685,1630]"`; -exports[`calculate simulations-salarié: frais pro - titres restaurant 1`] = `"[2519,0,0,2000,1521,1484]"`; +exports[`calculate simulations-salarié: frais pro - titres restaurant 1`] = `"[2519,0,2000,1521,1484]"`; -exports[`calculate simulations-salarié: frais pro - titres restaurant 2`] = `"[4307,0,0,3000,2134,1949]"`; +exports[`calculate simulations-salarié: frais pro - titres restaurant 2`] = `"[4307,0,3000,2134,1949]"`; -exports[`calculate simulations-salarié: frais pro - titres restaurant 3`] = `"[2562,0,0,2000,1493,1456]"`; +exports[`calculate simulations-salarié: frais pro - titres restaurant 3`] = `"[2562,0,2000,1493,1456]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 1`] = `"[2583,0,0,2000,1636,1599]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 1`] = `"[2583,0,2000,1636,1599]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 2`] = `"[3105,0,0,2000,2009,1965]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 2`] = `"[3105,0,2000,2009,1965]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 3`] = `"[2654,0,0,2000,1636,1599]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 3`] = `"[2654,0,2000,1636,1599]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 4`] = `"[2565,0,0,2000,1627,1590]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 4`] = `"[2565,0,2000,1627,1590]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 5`] = `"[3025,0,0,2000,1970,1932]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 5`] = `"[3025,0,2000,1970,1932]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 6`] = `"[3041,0,0,2000,1978,1939]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 6`] = `"[3041,0,2000,1978,1939]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 7`] = `"[3336,0,2446,2000,1919,1889]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 7`] = `"[3336,2446,2000,1919,1889]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 8`] = `"[3286,0,2286,2000,1889,1859]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 8`] = `"[3286,2286,2000,1889,1859]"`; -exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 1`] = `"[4076,0,0,3000,2353,2265]"`; +exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 1`] = `"[4076,0,3000,2353,2265]"`; -exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 2`] = `"[4076,0,0,3000,2353,2332]"`; +exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 2`] = `"[4076,0,3000,2353,2332]"`; -exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 3`] = `"[4076,0,0,3000,2353,2353]"`; +exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 3`] = `"[4076,0,3000,2353,2353]"`; -exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 4`] = `"[4076,0,0,3000,2353,2137]"`; +exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 4`] = `"[4076,0,3000,2353,2137]"`; -exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 5`] = `"[4076,0,0,3000,2353,2294]"`; +exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 5`] = `"[4076,0,3000,2353,2294]"`; -exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 6`] = `"[12890,0,0,9000,7156,6146]"`; +exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 6`] = `"[12890,0,9000,7156,6146]"`; -exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 7`] = `"[12890,0,0,9000,7156,6277]"`; +exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 7`] = `"[12890,0,9000,7156,6277]"`; -exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 8`] = `"[12890,0,0,9000,7156,6538]"`; +exports[`calculate simulations-salarié: impôt sur le revenu - quotient familial 8`] = `"[12890,0,9000,7156,6538]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 1`] = `"[4076,0,0,3000,2353,2168]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 1`] = `"[4076,0,3000,2353,2168]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 2`] = `"[41834,0,0,30000,24227,14685]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 2`] = `"[41834,0,30000,24227,14685]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 3`] = `"[4111,0,0,3000,2353,2172]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 3`] = `"[4111,0,3000,2353,2172]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 4`] = `"[3896,0,0,3000,2353,2252]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 4`] = `"[3896,0,3000,2353,2252]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 5`] = `"[41834,0,0,30000,24227,14685]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 5`] = `"[41834,0,30000,24227,14685]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 6`] = `"[4076,0,0,3000,2626,2481]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 6`] = `"[4076,0,3000,2626,2481]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 7`] = `"[41834,0,0,30000,26966,16383]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 7`] = `"[41834,0,30000,26966,16383]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 8`] = `"[4076,0,0,3000,2353,2107]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 8`] = `"[4076,0,3000,2353,2107]"`; -exports[`calculate simulations-salarié: inversions 1`] = `"[2000,0,0,1746,1360,1353]"`; +exports[`calculate simulations-salarié: inversions 1`] = `"[2000,0,1746,1360,1353]"`; -exports[`calculate simulations-salarié: inversions 2`] = `"[3474,0,0,2554,2000,1898]"`; +exports[`calculate simulations-salarié: inversions 2`] = `"[3474,0,2554,2000,1898]"`; -exports[`calculate simulations-salarié: inversions 3`] = `"[3679,0,0,2706,2120,2000]"`; +exports[`calculate simulations-salarié: inversions 3`] = `"[3679,0,2706,2120,2000]"`; -exports[`calculate simulations-salarié: lodeom 1`] = `"[1592,0,0,1521,1182,1182]"`; +exports[`calculate simulations-salarié: lodeom 1`] = `"[1592,0,1521,1182,1182]"`; -exports[`calculate simulations-salarié: lodeom 2`] = `"[2085,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: lodeom 2`] = `"[2085,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: lodeom 3`] = `"[3896,0,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: lodeom 3`] = `"[3896,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: lodeom 4`] = `"[5674,0,0,4000,3146,2759]"`; +exports[`calculate simulations-salarié: lodeom 4`] = `"[5674,0,4000,3146,2759]"`; -exports[`calculate simulations-salarié: lodeom 5`] = `"[7889,0,0,5500,4349,3625]"`; +exports[`calculate simulations-salarié: lodeom 5`] = `"[7889,0,5500,4349,3625]"`; -exports[`calculate simulations-salarié: lodeom compétitivité renforcée 1`] = `"[1592,0,0,1521,1182,1182]"`; +exports[`calculate simulations-salarié: lodeom compétitivité renforcée 1`] = `"[1592,0,1521,1182,1182]"`; -exports[`calculate simulations-salarié: lodeom compétitivité renforcée 2`] = `"[2085,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: lodeom compétitivité renforcée 2`] = `"[2085,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: lodeom compétitivité renforcée 3`] = `"[3444,0,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: lodeom compétitivité renforcée 3`] = `"[3444,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: lodeom compétitivité renforcée 4`] = `"[5588,0,0,4000,3146,2759]"`; +exports[`calculate simulations-salarié: lodeom compétitivité renforcée 4`] = `"[5588,0,4000,3146,2759]"`; -exports[`calculate simulations-salarié: lodeom compétitivité renforcée 5`] = `"[7889,0,0,5500,4349,3625]"`; +exports[`calculate simulations-salarié: lodeom compétitivité renforcée 5`] = `"[7889,0,5500,4349,3625]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 1`] = `"[1592,0,0,1521,1182,1182]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 1`] = `"[1592,0,1521,1182,1182]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 2`] = `"[2085,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 2`] = `"[2085,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 3`] = `"[3235,0,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 3`] = `"[3235,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 4`] = `"[4915,0,0,4000,3146,2759]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 4`] = `"[4915,0,4000,3146,2759]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 5`] = `"[7889,0,0,5500,4349,3625]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 5`] = `"[7889,0,5500,4349,3625]"`; -exports[`calculate simulations-salarié: stage 1`] = `"[507,0,0,500,500,500]"`; +exports[`calculate simulations-salarié: stage 1`] = `"[507,0,500,500,500]"`; -exports[`calculate simulations-salarié: stage 2`] = `"[2490,0,0,2000,1750,1750]"`; +exports[`calculate simulations-salarié: stage 2`] = `"[2490,0,2000,1750,1750]"`; -exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 1`] = `"[1606,0,0,1521,1195,1195]"`; +exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 1`] = `"[1606,0,1521,1195,1195]"`; -exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 2`] = `"[3423,0,0,2500,1979,1880]"`; +exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 2`] = `"[3423,0,2500,1979,1880]"`; -exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 3`] = `"[1592,0,0,1521,1170,1170]"`; +exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 3`] = `"[1592,0,1521,1170,1170]"`; -exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 4`] = `"[3382,0,0,2500,1938,1844]"`; +exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 4`] = `"[3382,0,2500,1938,1844]"`; -exports[`calculate simulations-salarié: temps partiel 1`] = `"[2592,0,2188,2000,1561,1524]"`; +exports[`calculate simulations-salarié: temps partiel 1`] = `"[2592,2188,2000,1561,1524]"`; -exports[`calculate simulations-salarié: temps partiel 2`] = `"[2533,0,2500,1857,1448,1428]"`; +exports[`calculate simulations-salarié: temps partiel 2`] = `"[2533,2500,1857,1448,1428]"`; -exports[`calculate simulations-salarié: temps partiel 3`] = `"[1159,0,1750,1000,770,770]"`; +exports[`calculate simulations-salarié: temps partiel 3`] = `"[1159,1750,1000,770,770]"`; -exports[`calculate simulations-salarié: treizième mois 1`] = `"[3390,0,0,2300,1950,1856]"`; +exports[`calculate simulations-salarié: treizième mois 1`] = `"[3390,0,2300,1950,1856]"`; -exports[`calculate simulations-salarié: treizième mois 2`] = `"[3800,0,2965,2300,2186,2073]"`; +exports[`calculate simulations-salarié: treizième mois 2`] = `"[3800,2965,2300,2186,2073]"`; -exports[`calculate simulations-salarié: treizième mois 3`] = `"[3044,0,0,2300,1799,1726]"`; +exports[`calculate simulations-salarié: treizième mois 3`] = `"[3044,0,2300,1799,1726]"`; -exports[`calculate simulations-salarié: échelle de salaires 1`] = `"[130,0,0,100,57,57]"`; +exports[`calculate simulations-salarié: échelle de salaires 1`] = `"[130,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 2`] = `"[284,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 3`] = `"[541,0,500,374,374]"`; -exports[`calculate simulations-salarié: échelle de salaires 4`] = `"[799,0,0,750,572,572]"`; +exports[`calculate simulations-salarié: échelle de salaires 4`] = `"[799,0,750,572,572]"`; -exports[`calculate simulations-salarié: échelle de salaires 5`] = `"[1056,0,0,1000,770,770]"`; +exports[`calculate simulations-salarié: échelle de salaires 5`] = `"[1056,0,1000,770,770]"`; -exports[`calculate simulations-salarié: échelle de salaires 6`] = `"[1313,0,0,1250,968,968]"`; +exports[`calculate simulations-salarié: échelle de salaires 6`] = `"[1313,0,1250,968,968]"`; -exports[`calculate simulations-salarié: échelle de salaires 7`] = `"[1571,0,0,1500,1165,1165]"`; +exports[`calculate simulations-salarié: échelle de salaires 7`] = `"[1571,0,1500,1165,1165]"`; -exports[`calculate simulations-salarié: échelle de salaires 8`] = `"[2479,0,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: échelle de salaires 8`] = `"[2479,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: échelle de salaires 9`] = `"[3401,0,0,2500,1957,1861]"`; +exports[`calculate simulations-salarié: échelle de salaires 9`] = `"[3401,0,2500,1957,1861]"`; -exports[`calculate simulations-salarié: échelle de salaires 10`] = `"[4076,0,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: échelle de salaires 10`] = `"[4076,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: échelle de salaires 11`] = `"[5674,0,0,4000,3146,2759]"`; +exports[`calculate simulations-salarié: échelle de salaires 11`] = `"[5674,0,4000,3146,2759]"`; -exports[`calculate simulations-salarié: échelle de salaires 12`] = `"[7085,0,0,5000,3948,3336]"`; +exports[`calculate simulations-salarié: échelle de salaires 12`] = `"[7085,0,5000,3948,3336]"`; -exports[`calculate simulations-salarié: échelle de salaires 13`] = `"[14319,0,0,10000,7958,6080]"`; +exports[`calculate simulations-salarié: échelle de salaires 13`] = `"[14319,0,10000,7958,6080]"`; -exports[`calculate simulations-salarié: échelle de salaires 14`] = `"[28345,0,0,20000,15969,10681]"`; +exports[`calculate simulations-salarié: échelle de salaires 14`] = `"[28345,0,20000,15969,10681]"`; -exports[`calculate simulations-salarié: échelle de salaires 15`] = `"[128575,0,0,100000,87157,46271]"`; +exports[`calculate simulations-salarié: échelle de salaires 15`] = `"[128575,0,100000,87157,46271]"`; -exports[`calculate simulations-salarié: échelle de salaires 16`] = `"[1243819,0,0,1000000,896257,446123]"`; +exports[`calculate simulations-salarié: échelle de salaires 16`] = `"[1243819,0,1000000,896257,446123]"`; diff --git a/test/regressions/simulations-artiste-auteur.yaml b/test/regressions/simulations-artiste-auteur.yaml index 3778c90f2..3e5123d3d 100644 --- a/test/regressions/simulations-artiste-auteur.yaml +++ b/test/regressions/simulations-artiste-auteur.yaml @@ -1,12 +1,12 @@ salarié: - - artiste-auteur . revenus . traitements et salaires: 1000 - - artiste-auteur . revenus . traitements et salaires: 10000 - - artiste-auteur . revenus . traitements et salaires: 100000 + - artiste-auteur . revenus . traitements et salaires: 1000 €/an + - artiste-auteur . revenus . traitements et salaires: 10000 €/an + - artiste-auteur . revenus . traitements et salaires: 100000 €/an bnc: - - artiste-auteur . revenus . BNC . recettes: 10000 - - artiste-auteur . revenus . BNC . recettes: 10000 + - artiste-auteur . revenus . BNC . recettes: 10000 €/an + - artiste-auteur . revenus . BNC . recettes: 10000 €/an artiste-auteur . revenus . BNC . micro-bnc: non - - artiste-auteur . revenus . BNC . recettes: 10000 + - artiste-auteur . revenus . BNC . recettes: 10000 €/an artiste-auteur . revenus . BNC . micro-bnc: non - artiste-auteur . revenus . BNC . frais réels: 5000 + artiste-auteur . revenus . BNC . frais réels: 5000 €/an diff --git a/test/regressions/simulations-auto-entrepreneur.yaml b/test/regressions/simulations-auto-entrepreneur.yaml index 1a7e4d24a..742bc6753 100644 --- a/test/regressions/simulations-auto-entrepreneur.yaml +++ b/test/regressions/simulations-auto-entrepreneur.yaml @@ -1,33 +1,33 @@ échelle de revenus: - - dirigeant . auto-entrepreneur . net de cotisations: 500 - - dirigeant . auto-entrepreneur . net de cotisations: 1000 - - dirigeant . auto-entrepreneur . net de cotisations: 2000 - - dirigeant . auto-entrepreneur . net de cotisations: 5000 - - dirigeant . auto-entrepreneur . net de cotisations: 10000 - - dirigeant . auto-entrepreneur . net de cotisations: 20000 - - dirigeant . auto-entrepreneur . net de cotisations: 50000 - - dirigeant . auto-entrepreneur . net de cotisations: 70000 - - dirigeant . auto-entrepreneur . net de cotisations: 100000 - - dirigeant . auto-entrepreneur . net de cotisations: 1000000 + - dirigeant . auto-entrepreneur . net de cotisations: 500 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 1000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 2000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 5000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 10000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 20000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 50000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 70000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 100000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 1000000 €/an aides: - - dirigeant . auto-entrepreneur . net de cotisations: 5000 + - dirigeant . auto-entrepreneur . net de cotisations: 5000 €/an entreprise . ACRE: oui - - dirigeant . auto-entrepreneur . net de cotisations: 50000 + - dirigeant . auto-entrepreneur . net de cotisations: 50000 €/an entreprise . ACRE: oui impôt sur le revenu: - - dirigeant . auto-entrepreneur . net de cotisations: 25000 - entreprise . catégorie d'activité: 'libérale' + - dirigeant . auto-entrepreneur . net de cotisations: 25000 €/an + entreprise . catégorie d'activité: "'libérale'" dirigeant . auto-entrepreneur . impôt . versement libératoire: oui ACRE: - - dirigeant . auto-entrepreneur . net de cotisations: 20000 + - dirigeant . auto-entrepreneur . net de cotisations: 20000 €/an entreprise . date de création: 01/01/2020 entreprise . ACRE: oui - - dirigeant . auto-entrepreneur . net de cotisations: 30000 + - dirigeant . auto-entrepreneur . net de cotisations: 30000 €/an entreprise . date de création: 01/06/2019 entreprise . ACRE: oui - - dirigeant . auto-entrepreneur . net de cotisations: 40000 + - dirigeant . auto-entrepreneur . net de cotisations: 40000 €/an entreprise . date de création: 01/06/2018 entreprise . ACRE: oui diff --git a/test/regressions/simulations-indépendant.yaml b/test/regressions/simulations-indépendant.yaml index 56c77f8d1..d94241bfe 100644 --- a/test/regressions/simulations-indépendant.yaml +++ b/test/regressions/simulations-indépendant.yaml @@ -1,45 +1,45 @@ échelle de revenus: - - dirigeant . indépendant . revenu net de cotisations: 500 - - dirigeant . indépendant . revenu net de cotisations: 1000 - - dirigeant . indépendant . revenu net de cotisations: 1500 - - dirigeant . indépendant . revenu net de cotisations: 2000 - - dirigeant . indépendant . revenu net de cotisations: 5000 - - dirigeant . indépendant . revenu net de cotisations: 10000 - - dirigeant . indépendant . revenu net de cotisations: 100000 - - dirigeant . indépendant . revenu net de cotisations: 1000000 + - dirigeant . indépendant . revenu net de cotisations: 500 €/an + - dirigeant . indépendant . revenu net de cotisations: 1000 €/an + - dirigeant . indépendant . revenu net de cotisations: 1500 €/an + - dirigeant . indépendant . revenu net de cotisations: 2000 €/an + - dirigeant . indépendant . revenu net de cotisations: 5000 €/an + - dirigeant . indépendant . revenu net de cotisations: 10000 €/an + - dirigeant . indépendant . revenu net de cotisations: 100000 €/an + - dirigeant . indépendant . revenu net de cotisations: 1000000 €/an inversions: - - dirigeant . rémunération totale: 2000 - - dirigeant . rémunération totale: 50000 - - revenu net après impôt: 10000 - - revenu net après impôt: 50000 - - revenu net après impôt: 10000 - entreprise . charges: 1000 - - entreprise . chiffre d'affaires minimum: 20000 - entreprise . charges: 1000 - - entreprise . chiffre d'affaires minimum: 20000 - entreprise . charges: 2000 + - dirigeant . rémunération totale: 2000 €/an + - dirigeant . rémunération totale: 50000 €/an + - revenu net après impôt: 10000 €/an + - revenu net après impôt: 50000 €/an + - revenu net après impôt: 10000 €/an + entreprise . charges: 1000 €/an + - entreprise . chiffre d'affaires minimum: 20000 €/an + entreprise . charges: 1000 €/an + - entreprise . chiffre d'affaires minimum: 20000 €/an + entreprise . charges: 2000 €/an cotisations minimales: - - dirigeant . indépendant . revenu net de cotisations: 100 - - dirigeant . indépendant . revenu net de cotisations: 100 + - dirigeant . indépendant . revenu net de cotisations: 100 €/an + - dirigeant . indépendant . revenu net de cotisations: 100 €/an situation personnelle . RSA: oui activité: - - dirigeant . indépendant . revenu net de cotisations: 20000 - entreprise . catégorie d'activité: libérale - - dirigeant . indépendant . revenu net de cotisations: 20000 - entreprise . catégorie d'activité: artisanale + - dirigeant . indépendant . revenu net de cotisations: 20000 €/an + entreprise . catégorie d'activité: "'libérale'" + - dirigeant . indépendant . revenu net de cotisations: 20000 €/an + entreprise . catégorie d'activité: "'artisanale'" acre: - - dirigeant . indépendant . revenu net de cotisations: 50000 + - dirigeant . indépendant . revenu net de cotisations: 50000 €/an entreprise . ACRE: true impôt sur le revenu: - - dirigeant . indépendant . revenu net de cotisations: 20000 - impôt . méthode de calcul: taux neutre - - dirigeant . indépendant . revenu net de cotisations: 50000 - impôt . méthode de calcul: taux neutre - - dirigeant . indépendant . revenu net de cotisations: 20000 - impôt . méthode de calcul: taux personnalisé + - dirigeant . indépendant . revenu net de cotisations: 20000 €/an + impôt . méthode de calcul: "'taux neutre'" + - dirigeant . indépendant . revenu net de cotisations: 50000 €/an + impôt . méthode de calcul: "'taux neutre'" + - dirigeant . indépendant . revenu net de cotisations: 20000 €/an + impôt . méthode de calcul: "'taux personnalisé'" impôt . taux personnalisé: 10 diff --git a/test/regressions/simulations-rémunération-dirigeant.yaml b/test/regressions/simulations-rémunération-dirigeant.yaml index 9ca07d898..104f87467 100644 --- a/test/regressions/simulations-rémunération-dirigeant.yaml +++ b/test/regressions/simulations-rémunération-dirigeant.yaml @@ -1,74 +1,74 @@ échelle de rémunération: - - dirigeant . rémunération totale: 100 - - dirigeant . rémunération totale: 1000 - - dirigeant . rémunération totale: 2000 - - dirigeant . rémunération totale: 5000 - - dirigeant . rémunération totale: 10000 - - dirigeant . rémunération totale: 20000 - - dirigeant . rémunération totale: 50000 - - dirigeant . rémunération totale: 100000 + - dirigeant . rémunération totale: 100 €/an + - dirigeant . rémunération totale: 1000 €/an + - dirigeant . rémunération totale: 2000 €/an + - dirigeant . rémunération totale: 5000 €/an + - dirigeant . rémunération totale: 10000 €/an + - dirigeant . rémunération totale: 20000 €/an + - dirigeant . rémunération totale: 50000 €/an + - dirigeant . rémunération totale: 100000 €/an avec charges: - - dirigeant . rémunération totale: 10000 + - dirigeant . rémunération totale: 10000 €/an entreprise . charges: 2000 - - dirigeant . rémunération totale: 20000 + - dirigeant . rémunération totale: 20000 €/an entreprise . charges: 15000 ACRE: - - dirigeant . rémunération totale: 10000 + - dirigeant . rémunération totale: 10000 €/an entreprise . date de création: 01/01/2020 entreprise . ACRE: oui - - dirigeant . rémunération totale: 20000 + - dirigeant . rémunération totale: 20000 €/an entreprise . date de création: 01/01/2020 entreprise . ACRE: oui - - dirigeant . rémunération totale: 30000 + - dirigeant . rémunération totale: 30000 €/an entreprise . date de création: 01/06/2020 entreprise . ACRE: oui activités: - - dirigeant . rémunération totale: 20000 - entreprise . catégorie d'activité: libérale - - dirigeant . rémunération totale: 20000 - entreprise . catégorie d'activité: libérale + - dirigeant . rémunération totale: 20000 €/an + entreprise . catégorie d'activité: "'libérale'" + - dirigeant . rémunération totale: 20000 €/an + entreprise . catégorie d'activité: "'libérale'" entreprise . catégorie d'activité . libérale règlementée: oui - - dirigeant . rémunération totale: 20000 - entreprise . catégorie d'activité: artisanale - - dirigeant . rémunération totale: 20000 - entreprise . catégorie d'activité: commerciale ou industrielle - entreprise . catégorie d'activité . service ou vente: vente - - dirigeant . rémunération totale: 20000 - entreprise . catégorie d'activité: commerciale ou industrielle - entreprise . catégorie d'activité . service ou vente: service + - dirigeant . rémunération totale: 20000 €/an + entreprise . catégorie d'activité: "'artisanale'" + - dirigeant . rémunération totale: 20000 €/an + entreprise . catégorie d'activité: "'commerciale ou industrielle'" + entreprise . catégorie d'activité . service ou vente: "'vente'" + - dirigeant . rémunération totale: 20000 €/an + entreprise . catégorie d'activité: "'commerciale ou industrielle'" + entreprise . catégorie d'activité . service ou vente: "'service'" entreprise . catégorie d'activité . restauration ou hébergement: oui Contrats Madelin: # Cas retraite: la cotisation Madelin est inferieure au plafond => le revenu net de # cotisations (résultat comptable) n'est pas affecté car l'assiette des # cotisations ne change pas: - - dirigeant . rémunération totale: 30000 + - dirigeant . rémunération totale: 30000 €/an entreprise . charges: 10000 dirigeant . indépendant . contrats madelin . mutuelle . montant: 4000 # plafond: 10% PSS donc environ 4100 # Cas retraite: la cotisation Madelin est supérieure au plafond => le revenu net de # cotisations est affecté car l'assiette des cotisations est plus élevée - - dirigeant . rémunération totale: 30000 + - dirigeant . rémunération totale: 30000 €/an entreprise . charges: 10000 dirigeant . indépendant . contrats madelin . mutuelle . montant: 5000 # plafond: 10% PSS donc environ 4100 # Cas mutuelle - - dirigeant . rémunération totale: 30000 + - dirigeant . rémunération totale: 30000 €/an entreprise . charges: 10000 dirigeant . indépendant . contrats madelin . mutuelle . montant: 1000 # Cas global madelin faible - - dirigeant . rémunération totale: 20000 + - dirigeant . rémunération totale: 20000 €/an entreprise . charges: 1000 dirigeant . indépendant . contrats madelin . mutuelle . montant: 200 dirigeant . indépendant . contrats madelin . retraite . montant: 300 # Cas global madelin grand (plafonds calculés différemment) - - dirigeant . rémunération totale: 300000 + - dirigeant . rémunération totale: 300000 €/an entreprise . charges: 15000 dirigeant . indépendant . contrats madelin . mutuelle . montant: 1500 dirigeant . indépendant . contrats madelin . retraite . montant: 5000 # Cas charges plus faibles que total madelin - - dirigeant . rémunération totale: 20000 + - dirigeant . rémunération totale: 20000 €/an entreprise . charges: 500 dirigeant . indépendant . contrats madelin . mutuelle . montant: 300 dirigeant . indépendant . contrats madelin . retraite . montant: 300 diff --git a/test/regressions/simulations-salarié.yaml b/test/regressions/simulations-salarié.yaml index 4c2a85ca6..a44712f11 100644 --- a/test/regressions/simulations-salarié.yaml +++ b/test/regressions/simulations-salarié.yaml @@ -1,139 +1,139 @@ échelle de salaires: - - contrat salarié . rémunération . brut de base: 100 - - contrat salarié . rémunération . brut de base: 250 - - contrat salarié . rémunération . brut de base: 500 - - contrat salarié . rémunération . brut de base: 750 - - contrat salarié . rémunération . brut de base: 1000 - - contrat salarié . rémunération . brut de base: 1250 - - contrat salarié . rémunération . brut de base: 1500 - - contrat salarié . rémunération . brut de base: 2000 - - contrat salarié . rémunération . brut de base: 2500 - - contrat salarié . rémunération . brut de base: 3000 - - contrat salarié . rémunération . brut de base: 4000 - - contrat salarié . rémunération . brut de base: 5000 - - contrat salarié . rémunération . brut de base: 10000 - - contrat salarié . rémunération . brut de base: 20000 - - contrat salarié . rémunération . brut de base: 100000 - - contrat salarié . rémunération . brut de base: 1000000 + - contrat salarié . rémunération . brut de base: 100 €/mois + - contrat salarié . rémunération . brut de base: 250 €/mois + - contrat salarié . rémunération . brut de base: 500 €/mois + - contrat salarié . rémunération . brut de base: 750 €/mois + - contrat salarié . rémunération . brut de base: 1000 €/mois + - contrat salarié . rémunération . brut de base: 1250 €/mois + - contrat salarié . rémunération . brut de base: 1500 €/mois + - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2500 €/mois + - contrat salarié . rémunération . brut de base: 3000 €/mois + - contrat salarié . rémunération . brut de base: 4000 €/mois + - contrat salarié . rémunération . brut de base: 5000 €/mois + - contrat salarié . rémunération . brut de base: 10000 €/mois + - contrat salarié . rémunération . brut de base: 20000 €/mois + - contrat salarié . rémunération . brut de base: 100000 €/mois + - contrat salarié . rémunération . brut de base: 1000000 €/mois effectif: - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois entreprise . effectif: 10 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois entreprise . effectif: 20 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois entreprise . effectif: 50 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois entreprise . effectif: 100 inversions: - - contrat salarié . prix du travail: 2000 - - contrat salarié . rémunération . net: 2000 - - contrat salarié . rémunération . net après impôt: 2000 + - contrat salarié . prix du travail: 2000 €/mois + - contrat salarié . rémunération . net: 2000 €/mois + - contrat salarié . rémunération . net après impôt: 2000 €/mois stage: - - contrat salarié: stage - contrat salarié . rémunération . brut de base: 500 - - contrat salarié: stage - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié: "'stage'" + contrat salarié . rémunération . brut de base: 500 €/mois + - contrat salarié: "'stage'" + contrat salarié . rémunération . brut de base: 2000 €/mois apprentissage: - - contrat salarié: apprentissage - contrat salarié . rémunération . brut de base: 1500 - - contrat salarié: apprentissage - contrat salarié . rémunération . brut de base: 1500 - contrat salarié . apprentissage . diplôme préparé: niveau bac ou moins - contrat salarié . apprentissage . ancienneté: moins de deux ans + - contrat salarié: "'apprentissage'" + contrat salarié . rémunération . brut de base: 1500 €/mois + - contrat salarié: "'apprentissage'" + contrat salarié . rémunération . brut de base: 1500 €/mois + contrat salarié . apprentissage . diplôme préparé: "'niveau bac ou moins'" + contrat salarié . apprentissage . ancienneté: "'moins de deux ans'" cadre: - - contrat salarié . rémunération . brut de base: 3000 + - contrat salarié . rémunération . brut de base: 3000 €/mois contrat salarié . statut cadre: oui cdd: - - contrat salarié: CDD - contrat salarié . rémunération . brut de base: 2000 - - contrat salarié: CDD - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié: "'CDD'" + contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié: "'CDD'" + contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . CDD . durée contrat: 6 contrat salarié . CDD . congés non pris: 3 - - contrat salarié: CDD - contrat salarié . rémunération . brut de base: 2400 + - contrat salarié: "'CDD'" + contrat salarié . rémunération . brut de base: 2400 €/mois contrat salarié . CDD . durée contrat: 10 contrat salarié . temps de travail . heures supplémentaires: 5 contrat salarié . indemnité kilométrique vélo: oui contrat salarié . avantages en nature . montant: 200 atmp: - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . ATMP . taux collectif ATMP: 5 assimilé salarié: - - dirigeant: assimilé salarié - contrat salarié . rémunération . brut de base: 5000 - - dirigeant: assimilé salarié - contrat salarié . rémunération . brut de base: 1500 + - dirigeant: "'assimilé salarié'" + contrat salarié . rémunération . brut de base: 5000 €/mois + - dirigeant: "'assimilé salarié'" + contrat salarié . rémunération . brut de base: 1500 €/mois entreprise . ACRE: oui - - dirigeant: assimilé salarié - contrat salarié . rémunération . brut de base: 3000 + - dirigeant: "'assimilé salarié'" + contrat salarié . rémunération . brut de base: 3000 €/mois entreprise . ACRE: oui aides: - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . statut JEI: oui - - contrat salarié . rémunération . brut de base: 10000 + - contrat salarié . rémunération . brut de base: 10000 €/mois contrat salarié . régime des impatriés: oui - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . aides employeur . emploi franc . éligible: oui - - contrat salarié: CDD - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié: "'CDD'" + contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . CDD . durée contrat: 6 contrat salarié . aides employeur . emploi franc . éligible: oui temps partiel: - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . temps partiel: oui - - contrat salarié . rémunération . brut de base . équivalent temps plein: 2500 + - contrat salarié . rémunération . brut de base . équivalent temps plein: 2500€/mois contrat salarié . temps de travail . temps partiel: oui contrat salarié . temps de travail . temps partiel . heures par semaine: 26 - - contrat salarié . rémunération . brut de base: 1000 + - contrat salarié . rémunération . brut de base: 1000 €/mois contrat salarié . temps de travail . temps partiel: oui contrat salarié . temps de travail . temps partiel . heures par semaine: 20 treizième mois: - - contrat salarié . rémunération . brut de base: 2300 + - contrat salarié . rémunération . brut de base: 2300 €/mois contrat salarié . rémunération . primes . fin d'année . treizième mois: oui - - contrat salarié . rémunération . brut de base: 2300 + - contrat salarié . rémunération . brut de base: 2300 €/mois contrat salarié . rémunération . primes . activité . base: 200 contrat salarié . rémunération . primes . fin d'année . treizième mois: oui contrat salarié . temps de travail . temps partiel: oui contrat salarié . temps de travail . temps partiel . heures par semaine: 26 contrat salarié . temps de travail . heures complémentaires: 5 - - contrat salarié . rémunération . brut de base: 2300 + - contrat salarié . rémunération . brut de base: 2300 €/mois contrat salarié . rémunération . primes . fin d'année . prime de fin d'année en mois: 2 impôt sur le revenu: - - contrat salarié . rémunération . brut de base: 3000 - impôt . méthode de calcul: taux neutre - - contrat salarié . rémunération . brut de base: 30000 - impôt . méthode de calcul: taux neutre - - contrat salarié: CDD - contrat salarié . rémunération . brut de base: 3000 + - contrat salarié . rémunération . brut de base: 3000 €/mois + impôt . méthode de calcul: "'taux neutre'" + - contrat salarié . rémunération . brut de base: 30000 €/mois + impôt . méthode de calcul: "'taux neutre'" + - contrat salarié: "'CDD'" + contrat salarié . rémunération . brut de base: 3000 €/mois contrat salarié . CDD . durée contrat: 2 - impôt . méthode de calcul: taux neutre - - contrat salarié . rémunération . brut de base: 3000 - impôt . méthode de calcul: taux neutre - établissement . localisation . département: Guadeloupe - - contrat salarié . rémunération . brut de base: 30000 - impôt . méthode de calcul: taux neutre - établissement . localisation . département: Guadeloupe - - contrat salarié . rémunération . brut de base: 3000 - impôt . méthode de calcul: taux neutre - établissement . localisation . département: Mayotte - - contrat salarié . rémunération . brut de base: 30000 - impôt . méthode de calcul: taux neutre - établissement . localisation . département: Mayotte - - contrat salarié . rémunération . brut de base: 3000 - impôt . méthode de calcul: taux personnalisé + impôt . méthode de calcul: "'taux neutre'" + - contrat salarié . rémunération . brut de base: 3000 €/mois + impôt . méthode de calcul: "'taux neutre'" + établissement . localisation . département: "'Guadeloupe'" + - contrat salarié . rémunération . brut de base: 30000 €/mois + impôt . méthode de calcul: "'taux neutre'" + établissement . localisation . département: "'Guadeloupe'" + - contrat salarié . rémunération . brut de base: 3000 €/mois + impôt . méthode de calcul: "'taux neutre'" + établissement . localisation . département: "'Mayotte'" + - contrat salarié . rémunération . brut de base: 30000 €/mois + impôt . méthode de calcul: "'taux neutre'" + établissement . localisation . département: "'Mayotte'" + - contrat salarié . rémunération . brut de base: 3000 €/mois + impôt . méthode de calcul: "'taux personnalisé'" impôt . taux personnalisé: 10 impôt sur le revenu - quotient familial: @@ -158,180 +158,180 @@ impôt sur le revenu - quotient familial: impôt . foyer fiscal . enfants à charge: 4 heures supplémentaires et complémentaires: - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . heures supplémentaires: 5 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . heures supplémentaires: 30 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . heures supplémentaires: 5 entreprise . effectif: 100 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . heures supplémentaires: 5 - contrat salarié . convention collective: 'HCR' - - contrat salarié . rémunération . brut de base: 2000 + contrat salarié . convention collective: "'HCR'" + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . heures supplémentaires: 30 - contrat salarié . convention collective: 'HCR' - - contrat salarié . rémunération . brut de base: 2000 + contrat salarié . convention collective: "'HCR'" + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . heures supplémentaires: 30 - contrat salarié . convention collective: 'compta' - - contrat salarié . rémunération . brut de base: 2000 + contrat salarié . convention collective: "'compta'" + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . temps partiel: oui contrat salarié . temps de travail . temps partiel . heures par semaine: 24 contrat salarié . temps de travail . heures complémentaires: 20 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . temps de travail . temps partiel: oui contrat salarié . temps de travail . temps partiel . heures par semaine: 26 contrat salarié . temps de travail . heures complémentaires: 20 avantages: - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . rémunération . avantages en nature: oui contrat salarié . rémunération . avantages en nature . montant: 100 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . rémunération . avantages en nature: oui contrat salarié . rémunération . avantages en nature . autres: oui contrat salarié . rémunération . avantages en nature . autres . montant: 100 contrat salarié . rémunération . avantages en nature . ntic . coût appareils: 400 contrat salarié . rémunération . avantages en nature . ntic . abonnements: 20 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . rémunération . avantages en nature: oui contrat salarié . rémunération . avantages en nature . nourriture: oui contrat salarié . rémunération . avantages en nature . nourriture . repas par mois: 10 JEI: - - contrat salarié . rémunération . brut de base: 3000 + - contrat salarié . rémunération . brut de base: 3000 €/mois contrat salarié . statut JEI: oui - - contrat salarié . rémunération . brut de base: 20000 + - contrat salarié . rémunération . brut de base: 20000 €/mois contrat salarié . statut JEI: oui - - contrat salarié . rémunération . brut de base: 4000 - dirigeant: 'assimilé salarié' + - contrat salarié . rémunération . brut de base: 4000 €/mois + dirigeant: "'assimilé salarié'" contrat salarié . statut JEI: oui frais pro - titres restaurant: - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . frais professionnels . titres-restaurant: oui contrat salarié . frais professionnels . titres-restaurant . titres-restaurant par mois: 10 - - contrat salarié . rémunération . brut de base: 3000 + - contrat salarié . rémunération . brut de base: 3000 €/mois contrat salarié . frais professionnels . titres-restaurant: oui contrat salarié . frais professionnels . titres-restaurant . titres-restaurant par mois: 20 contrat salarié . frais professionnels . titres-restaurant . montant unitaire: 20 - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . frais professionnels . titres-restaurant: oui contrat salarié . frais professionnels . titres-restaurant . taux participation employeur: 55 frais pro - IKV: - - contrat salarié . rémunération . brut de base: 3200 + - contrat salarié . rémunération . brut de base: 3200 €/mois contrat salarié . frais professionnels . indemnité kilométrique vélo: oui - - contrat salarié . rémunération . brut de base: 3200 + - contrat salarié . rémunération . brut de base: 3200 €/mois contrat salarié . frais professionnels . indemnité kilométrique vélo . distance mensuelle: 200 - - contrat salarié . rémunération . net après impôt: 1630 + - contrat salarié . rémunération . net après impôt: 1630 €/mois contrat salarié . frais professionnels . indemnité kilométrique vélo . distance mensuelle: 30 frais pro - DFS: - - contrat salarié . rémunération . brut de base: 2000 - contrat salarié . profession spécifique: journaliste - - contrat salarié . rémunération . brut de base: 2000 - contrat salarié . profession spécifique: ouvrier du bâtiment - - contrat salarié . rémunération . brut de base: 2000 - contrat salarié . profession spécifique: artiste musicien - - contrat salarié . rémunération . brut de base: 2000 - contrat salarié . profession spécifique: pilote de ligne ou personnel navigant - - contrat salarié . rémunération . brut de base: 2000 - contrat salarié . profession spécifique: journaliste + - contrat salarié . rémunération . brut de base: 2000 €/mois + contrat salarié . profession spécifique: "'journaliste'" + - contrat salarié . rémunération . brut de base: 2000 €/mois + contrat salarié . profession spécifique: "'ouvrier du bâtiment'" + - contrat salarié . rémunération . brut de base: 2000 €/mois + contrat salarié . profession spécifique: "'artiste musicien'" + - contrat salarié . rémunération . brut de base: 2000 €/mois + contrat salarié . profession spécifique: "'pilote de ligne ou personnel navigant'" + - contrat salarié . rémunération . brut de base: 2000 €/mois + contrat salarié . profession spécifique: "'journaliste'" contrat salarié . déduction forfaitaire spécifique . application: non # Test des taux réduits journalistes et abattement fiscal - - contrat salarié . rémunération . brut de base: 1700 - contrat salarié . profession spécifique: journaliste - - contrat salarié . rémunération . brut de base: 2600 - contrat salarié . profession spécifique: journaliste + - contrat salarié . rémunération . brut de base: 1700 €/mois + contrat salarié . profession spécifique: "'journaliste'" + - contrat salarié . rémunération . brut de base: 2600 €/mois + contrat salarié . profession spécifique: "'journaliste'" activité partielle: - - contrat salarié . rémunération . brut de base: 1560 + - contrat salarié . rémunération . brut de base: 1560 €/mois contrat salarié . activité partielle: oui - - contrat salarié . rémunération . brut de base: 4000 + - contrat salarié . rémunération . brut de base: 4000 €/mois contrat salarié . activité partielle: oui - - contrat salarié . rémunération . brut de base: 8000 + - contrat salarié . rémunération . brut de base: 8000 €/mois contrat salarié . activité partielle: oui - - contrat salarié . rémunération . brut de base: 4000 + - contrat salarié . rémunération . brut de base: 4000 €/mois contrat salarié . activité partielle: oui contrat salarié . activité partielle . heures travaillées: 30.33331 - - contrat salarié . rémunération . brut de base: 4000 + - contrat salarié . rémunération . brut de base: 4000 €/mois contrat salarié . activité partielle: oui contrat salarié . activité partielle . heures travaillées: 75.833275 - - contrat salarié . rémunération . brut de base: 3000 + - contrat salarié . rémunération . brut de base: 3000 €/mois contrat salarié . activité partielle: oui contrat salarié . temps de travail . temps partiel: oui contrat salarié . temps de travail . temps partiel . heures par semaine: 28 - - contrat salarié . rémunération . brut de base: 4000 + - contrat salarié . rémunération . brut de base: 4000 €/mois contrat salarié . activité partielle: oui - contrat salarié . profession spécifique: journaliste - - contrat salarié . rémunération . brut de base: 2000 + contrat salarié . profession spécifique: "'journaliste'" + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . activité partielle: oui contrat salarié . activité partielle . convention syntec: oui - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . activité partielle . heures travaillées: 75.833275 contrat salarié . activité partielle: oui contrat salarié . activité partielle . convention syntec: oui - - contrat salarié . rémunération . brut de base: 6000 + - contrat salarié . rémunération . brut de base: 6000 €/mois contrat salarié . activité partielle: oui contrat salarié . activité partielle . convention syntec: oui lodeom: - - contrat salarié . rémunération . brut de base: 1521.22 + - contrat salarié . rémunération . brut de base: 1521.22 €/mois contrat salarié . lodeom . zone un: oui - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . lodeom . zone un: oui - - contrat salarié . rémunération . brut de base: 3000 + - contrat salarié . rémunération . brut de base: 3000 €/mois contrat salarié . lodeom . zone un: oui - - contrat salarié . rémunération . brut de base: 4000 + - contrat salarié . rémunération . brut de base: 4000 €/mois contrat salarié . lodeom . zone un: oui - - contrat salarié . rémunération . brut de base: 5500 + - contrat salarié . rémunération . brut de base: 5500 €/mois contrat salarié . lodeom . zone un: oui lodeom compétitivité renforcée: - - contrat salarié . rémunération . brut de base: 1521.22 + - contrat salarié . rémunération . brut de base: 1521.22 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème compétitivité renforcée: oui - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème compétitivité renforcée: oui - - contrat salarié . rémunération . brut de base: 3000 + - contrat salarié . rémunération . brut de base: 3000 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème compétitivité renforcée: oui - - contrat salarié . rémunération . brut de base: 4000 + - contrat salarié . rémunération . brut de base: 4000 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème compétitivité renforcée: oui - - contrat salarié . rémunération . brut de base: 5500 + - contrat salarié . rémunération . brut de base: 5500 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème compétitivité renforcée: oui lodeom innovation et croissance: - - contrat salarié . rémunération . brut de base: 1521.22 + - contrat salarié . rémunération . brut de base: 1521.22 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème innovation et croissance: oui - - contrat salarié . rémunération . brut de base: 2000 + - contrat salarié . rémunération . brut de base: 2000 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème innovation et croissance: oui - - contrat salarié . rémunération . brut de base: 3000 + - contrat salarié . rémunération . brut de base: 3000 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème innovation et croissance: oui - - contrat salarié . rémunération . brut de base: 4000 + - contrat salarié . rémunération . brut de base: 4000 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème innovation et croissance: oui - - contrat salarié . rémunération . brut de base: 5500 + - contrat salarié . rémunération . brut de base: 5500 €/mois contrat salarié . lodeom . zone un: oui contrat salarié . lodeom . éligible barème innovation et croissance: oui taux spécifiques retraite complémentaire: - - contrat salarié . rémunération . brut de base: 1521.22 + - contrat salarié . rémunération . brut de base: 1521.22 €/mois contrat salarié . retraite complémentaire . taux employeur tranche 1: 5.59 contrat salarié . retraite complémentaire . taux salarié tranche 1: 2.28 - - contrat salarié . rémunération . brut de base: 2500 + - contrat salarié . rémunération . brut de base: 2500 €/mois contrat salarié . retraite complémentaire . taux employeur tranche 1: 5.59 contrat salarié . retraite complémentaire . taux salarié tranche 1: 2.28 - - contrat salarié . rémunération . brut de base: 1521.22 + - contrat salarié . rémunération . brut de base: 1521.22 €/mois contrat salarié . retraite complémentaire . taux employeur tranche 1: 3.94 contrat salarié . retraite complémentaire . taux salarié tranche 1: 3.93 - - contrat salarié . rémunération . brut de base: 2500 + - contrat salarié . rémunération . brut de base: 2500 €/mois contrat salarié . retraite complémentaire . taux employeur tranche 1: 3.94 contrat salarié . retraite complémentaire . taux salarié tranche 1: 3.93 diff --git a/test/regressions/simulations.jest.js b/test/regressions/simulations.jest.js index 638c47f0e..7dbb82f73 100644 --- a/test/regressions/simulations.jest.js +++ b/test/regressions/simulations.jest.js @@ -20,25 +20,16 @@ import remunerationDirigeantSituations from './simulations-rémunération-dirige import employeeSituations from './simulations-salarié.yaml' const roundResult = arr => arr.map(x => Math.round(x)) -const engine = new Engine({ rules }) -const runSimulations = ( - situations, - targets, - baseSituation = {}, - defaultUnits, - namePrefix = '' -) => +const engine = new Engine(rules) +const runSimulations = (situations, targets, baseSituation = {}) => Object.entries(situations).map(([name, situations]) => situations.forEach(situation => { engine.setSituation({ ...baseSituation, ...situation }) - engine.setDefaultUnits(defaultUnits) - const res = engine.evaluate(targets).map(node => node.nodeValue) + const res = targets.map(target => engine.evaluate(target).nodeValue) // 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 - ) + expect(JSON.stringify(roundResult(res))).toMatchSnapshot(name) }) ) @@ -46,8 +37,7 @@ it('calculate simulations-salarié', () => { runSimulations( employeeSituations, employeeConfig.objectifs, - employeeConfig.situation, - ['€/mois'] + employeeConfig.situation ) }) @@ -56,38 +46,57 @@ it('calculate simulations-indépendant', () => { (acc, cur) => [...acc, ...cur.objectifs], [] ) - runSimulations(independentSituations, targets, independantConfig.situation, [ - '€/an' - ]) + runSimulations(independentSituations, targets, independantConfig.situation) }) it('calculate simulations-auto-entrepreneur', () => { runSimulations( autoEntrepreneurSituations, autoentrepreneurConfig.objectifs, - autoentrepreneurConfig.situation, - ['€/an'] + autoentrepreneurConfig.situation ) }) -it('calculate simulations-rémunération-dirigeant', () => { - const baseSituation = remunerationDirigeantConfig.situation - remunerationDirigeantConfig.branches.forEach(({ nom, situation }) => { - runSimulations( - remunerationDirigeantSituations, - remunerationDirigeantConfig.objectifs, - { ...baseSituation, ...situation }, - ['€/an'], - `${nom} - ` - ) - }) +it('calculate simulations-rémunération-dirigeant (assimilé salarié)', () => { + runSimulations( + remunerationDirigeantSituations, + remunerationDirigeantConfig.objectifs, + { + ...remunerationDirigeantConfig.situation, + dirigeant: "'assimilé salarié'" + }, + 'assimilé salarié' + ) +}) + +it('calculate simulations-rémunération-dirigeant (auto-entrepreneur)', () => { + runSimulations( + remunerationDirigeantSituations, + remunerationDirigeantConfig.objectifs, + { + ...remunerationDirigeantConfig.situation, + dirigeant: "'auto-entrepreneur'" + }, + 'auto-entrepreneur' + ) +}) + +it('calculate simulations-rémunération-dirigeant (indépendant)', () => { + runSimulations( + remunerationDirigeantSituations, + remunerationDirigeantConfig.objectifs, + { + ...remunerationDirigeantConfig.situation, + dirigeant: "'indépendant'" + }, + 'indépendant' + ) }) it('calculate simulations-artiste-auteur', () => { runSimulations( artisteAuteurSituations, artisteAuteurConfig.objectifs, - artisteAuteurConfig.situation, - ['€/an'] + artisteAuteurConfig.situation ) }) diff --git a/test/rules/co2.yaml b/test/rules/co2.yaml index b0da5e3e4..5a4e93422 100644 --- a/test/rules/co2.yaml +++ b/test/rules/co2.yaml @@ -8,8 +8,7 @@ douche . impact: douche . nombre: question: Combien prenez-vous de douches ? - unité: douche - par défaut: 30 + par défaut: 30 douches suggestions: Une par jour: 30 @@ -54,7 +53,7 @@ chauffage . type: - gaz - fioul - électricité - par défaut: gaz + par défaut: "'gaz'" chauffage . type . gaz: icônes: 🔵 @@ -102,8 +101,7 @@ chauffage . impact par litre: douche . durée de la douche: question: Combien de temps dure votre douche en général ? - unité: min - par défaut: 5 + par défaut: 5 min suggestions: expresse: 5 moyenne: 10 diff --git a/test/rules/sasu.yaml b/test/rules/sasu.yaml index 55de08d28..86bb8ecce 100644 --- a/test/rules/sasu.yaml +++ b/test/rules/sasu.yaml @@ -3,16 +3,13 @@ # dans la base centrale chiffre affaires: - unité par défaut: €/mois - par défaut: 0 + par défaut: 0 €/mois charges: - unité: €/mois - par défaut: 0 + par défaut: 0 €/mois répartition salaire sur dividendes: - par défaut: 50 - unité: '%' + par défaut: 50% impôt sur les sociétés: formule: diff --git a/test/tree.test.js b/test/tree.test.js deleted file mode 100644 index b81c9fd3a..000000000 --- a/test/tree.test.js +++ /dev/null @@ -1,439 +0,0 @@ -import * as R from 'ramda' -import { expect } from 'chai' -import daggy from 'daggy' -import { Maybe as M } from 'ramda-fantasy' -import { StateT, Writer } from 'akh' - -describe('simplified tree walks', function() { - // Notre domaine peut se simplifier à une liste d'équations à trous: - // a: 45 - // b: a + c - // d: a + 4 - // e: b + d - // Disons que je veux connaitre "e", alors il va me manquer "c" - // Si je connais "c", alors je peux calculer "e" - // Et mon ambition est aussi de pouvoir visualiser le calcul en HTML - // Donc j'ai une structure plate que je transforme en arbre (ce n'est pas - // le focus de la présente exploration), je veux pouvoir demander des choses - // diverses à cet arbre: l'évaluer, repérer les trous, le transformer en HTML - - // Plus tard je vais avoir des trucs plus sophistiqués, par exemple: - // b: a + (bleu: b, vert: c) - // qui est équivalent à: - // b: b-bleu + b-vert - // b-bleu: a + b - // b-vert: a + c - // Le but du jeu est de pouvoir le représenter de façon compacte, mais - // d'avoir un arbre simple à manipuler - - // Pour intégrer dans le simulateur, il faut remplir les exigences - // suivantes: - // X décorer l'arbre avec une valeur à chaque noeud - // X réaliser le calcul de façon efficiente (1 fois par variable) - // - savoir "court-circuiter" le calcul de variables manquantes dans les conditionnelles - // - avoir un moyen de gérer les composantes et filtrage - - // Ce qu'on décrit est un framework de programmation déclarative: on stipule des - // définitions (salaire net = brut - cotisations) mais on les donne sans ordre - // impératif, on laisse au moteur le soin de calculer les dépendances - - // Chaque élément de notre base de règles est une définition: - - const Def = daggy.taggedSum('Def', { - Assign: ['name', 'expr'] - }) - const { Assign } = Def - - // Par contre, à l'exécution, il faut bien calculer des "effets de bord" - // pour rester performant: chaque évaluation d'une définition doit mettre - // à jour le 'dictionnaire' des valeurs connues, puis le mettre à disposition - // de la suite du calcul - on verra comment au Chapitre 3 - - // La partie droite d'une définition est une expression: - - const Expr = daggy.taggedSum('Expr', { - Num: ['x'], - Add: ['x', 'y'], - Var: ['name'] - // NotIf: ['condition','formule'], - // OnlyIf: ['condition','formule'], - // AnyOf: ['conditions'], - // AllOf: ['conditions'], - }) - const { Num, Add, Var } = Expr - - // Chapitre 1... - - // Le type Expr est la traduction en JS du type suivant en Haskell, - // "naivement récursif": - // data Expr = Num Int | Var String | Add Expr Expr - - // Il se trouve qu'on peut gagner beaucoup en introduisant une petite - // complexité: on va exprimer la récursion avec un niveau d'indirection, - // la première étape étant de rendre le type polymorphique sur ce qui - // est récursif: - - // data ExprF r = Num Int | Var String | Add r r - - // Par exemple, une addition de deux additions c'est de type ExprF (ExprF r), - // et si je veux décrire des imbrications plus poussées d'additions dans - // des additions il me faudra un ExprF (ExprF (ExprF r)) et ainsi de - // suite: on a "déroulé" la récursion dans le type d'origine. - - // On peut alors retrouver le type d'origine en introduisant un - // "constructeur de point fixe de type", appelé Fx, et en introduisant - // ce qu'on appelle un "functor type" (c'est le suffixe F) - - // data Expr = Fx ExprF - - // Le point fixe de f est une solution à l'équation x = f x - on - // peut l'appliquer à des fonctions récursives, voir par exemple: - // https://www.vex.net/~trebla/haskell/fix.xhtml - - // En JS ça ne marche pas parce que JS est strict et non lazy... - - // Quand au point fixe d'un type, c'est le point fixe de son - // constructeur: une solution à l'équation T = Fx T - - // En JS c'est juste une fonction qui emballe et une qui déballe: - - const Fx = daggy.tagged('Fx', ['x']) - Fx.prototype.project = function() { - return this.x - } - const unFix = fx => fx.project() - - // Les helpers suivants rendent moins pénible la construction de valeurs - // notamment pour les tests - - let num = x => Fx(Num(x)) - let add = (x, y) => Fx(Add(x, y)) - let ref = name => Fx(Var(name)) - - // Une application de la théorie des catégories permet de dériver - // la fonction "fold" suivante, qui généralise aux structures récursives - // la notion de "reduction" (comme pour les listes), on l'appelle aussi - // un catamorphisme - - // fold :: Functor f => (f a -> a) -> Fix f -> a - const fold = R.curry((algebra, x) => - R.compose(algebra, R.map(fold(algebra)), unFix)(x) - ) - - // Cf. https://www.schoolofhaskell.com/user/bartosz/understanding-algebras - - // Dans ce contexte, un "algebre" est une fonction qui nous dit comment calculer - // la réduction pour un noeud à partir des valeurs calculées pour les noeuds fils - - // Cette fonction fournit la traversée - Expr.prototype.map = function(f) { - return this.cata({ - Num: x => this, // fixed - Add: (x, y) => Add(f(x), f(y)), - Var: name => this - }) - } - - // Celle-ci l'évaluation - const evaluator = state => a => { - return a.cata({ - Num: x => M.Just(x), - Add: (x, y) => R.lift(R.add)(x, y), - Var: name => M.toMaybe(state[name]) // Doesn't typecheck - }) - } - - let evaluate = (expr, state = {}) => - fold(evaluator(state), expr).getOrElse(null) // for convenience - - // Voici donc l'évaluation d'un arbre... - - it('should provide a protocol for evaluation', function() { - let tree = num(45), - result = evaluate(tree) - expect(result).to.equal(45) - }) - - it('should evaluate expressions', function() { - let tree = add(num(45), num(25)), - result = evaluate(tree) - expect(result).to.equal(70) - }) - - it('should evaluate nested expressions', function() { - let tree = add(num(45), add(num(15), num(10))), - result = evaluate(tree) - expect(result).to.equal(70) - }) - - // Problème: on évalue l'arbre tout entier d'un seul coup; mais - // peut-on aussi "décorer" l'arbre pendant sa traversée avec les - // valeurs intermédiaires ? On verra que oui, au Chapitre 2; en - // attendant on voudrait aussi savoir quelles sont les variables - // manquantes... - - const collector = state => a => { - return a.cata({ - Num: x => [], - Add: (x, y) => R.concat(x, y), - Var: name => (state[name] ? [] : [name]) - }) - } - - let missing = (expr, state = {}) => fold(collector(state), expr) - - it('should evaluate expressions involving variables', function() { - let tree = add(num(45), ref('a')), - result = evaluate(tree, { a: 25 }) - expect(result).to.equal(70) - }) - - it('should evaluate expressions involving missing variables', function() { - let tree = add(num(45), ref('b')), - result = evaluate(tree, { a: 25 }) - expect(result).to.equal(null) - }) - - it('should provide a protocol for missing variables', function() { - let tree = ref('a'), - result = missing(tree) - expect(result).to.deep.equal(['a']) - }) - - it('should locate missing variables in expressions', function() { - let tree = add(num(45), ref('a')), - result = missing(tree) - expect(result).to.deep.equal(['a']) - }) - - it('should locate missing variables in nested expressions', function() { - let tree = add(add(num(35), ref('a')), num(25)), - result = missing(tree) - expect(result).to.deep.equal(['a']) - }) - - it('should locate missing variables in nested expressions', function() { - let tree = add(add(num(35), ref('a')), num(25)), - result = missing(tree, { a: 25 }) - expect(result).to.deep.equal([]) - }) - - // Chapitre 2... - - // Pour annoter l'arbre avec les valeurs intermédiaires on utilise un - // type "Cofree Comonad": ce sont des paires (fst,snd) dont la première - // valeur est un noeud de l'arbre et la seconde l'annotation; on a un - // constructeur ann et une fonction de lecture - - // Cf https://github.com/willtim/recursion-schemes/ - // or http://www.timphilipwilliams.com/slides/HaskellAtBarclays.pdf - - const AnnF = daggy.tagged('AnnF', ['fr', 'a']) - let ann = ({ fst, snd }) => Fx(AnnF(fst, snd)) - let nodeValue = annf => { - let { fr, a } = unFix(annf) - return a - } - - // fork est l'opérateur "&&&" de Haskell: (f &&& g) x = Pair(f(x),g(x)) - let fork = (f, g) => x => ({ fst: f(x), snd: g(x) }) - - // synthesize combine l'application d'un algèbre fourni f et de l'annotation - let synthesize = f => { - let algebra = f => - R.compose(ann, fork(R.identity, R.compose(f, R.map(nodeValue)))) - return fold(algebra(f)) - } - - let annotate = (state, tree) => synthesize(evaluator(state))(tree) - - it('should annotate tree with evaluation results', function() { - let tree = add(num(45), add(num(15), num(10))), - result = nodeValue(annotate({}, tree)).getOrElse(null) - expect(result).to.equal(70) - }) - - // Chapitre 3 - - // On sait evaluer des expressions, il faut aussi être capable de - // gérer les règles définissant les variables appelées dans ces - // expressions; voyons ce que ça donne avec un algèbre plus simple: - - let calculate = R.curry((rules, name) => { - let find = (rules, name) => - R.find(x => R.prop('name', x) == name, rules).expr, - expr = find(rules, name) - return fold(evaluator2(calculate(rules)), expr) - }) - - const evaluator2 = calculate => a => { - return a.cata({ - Num: x => x, - Add: (x, y) => x + y, - Var: name => calculate(name) - }) - } - - it('should resolve variable dependencies', function() { - let rule1 = Assign('a', add(ref('b'), ref('b'))), - rule2 = Assign('b', num(15)), - rules = [rule1, rule2], - result = calculate(rules, 'a') - expect(result).to.equal(30) - }) - - // Utilisons un Writer (un idiome fonctionnel pour par exemple écrire des logs) - // pour examiner le calcul de plus près. - - const Str = daggy.tagged('Str', ['s']) - Str.zero = Str('') - Str.prototype.zero = Str.zero - Str.prototype.concat = function(b) { - return Str(this.s + b.s) - } - - let trace = R.curry((rules, name) => { - let find = (rules, name) => - R.find(x => R.prop('name', x) == name, rules).expr, - expr = find(rules, name) - return fold(tracer(trace(rules)), expr) - }) - - const tracer = recurse => a => { - let log = (x, s) => Writer.tell(Str(s)).map(_ => x) - return a.cata({ - Num: x => log(x, x + ','), - Add: (x, y) => x.chain(xx => y.chain(yy => log(xx + yy, '+,'))), - Var: name => recurse(name).chain(x => log(x, name + ',')) - }) - } - - // On voit qu'on a calculé la valeur de b 2 fois! Ce n'est pas utile, - // puisque cette valeur ne changera pas au cours du calcul; et comme on - // répète le calcul autant de fois qu'il y a de références à une variable - // donnée, si l'arbre est un tant soit peu complexe les performances seront - // très mauvaises. - - it('should trace the shape of the computation', function() { - let rule1 = Assign('a', add(ref('b'), ref('b'))), - rule2 = Assign('b', num(15)), - rules = [rule1, rule2], - result = trace(rules, 'a').run(Str.zero) - expect(result.value).to.equal(30) - expect(result.output.s).to.equal('15,b,15,b,+,') - }) - - // Pour corriger ce problème on va avoir besoin de formuler une version - // "monadique" du catamorphisme, c'est-à-dire qu'on va pouvoir l'associer - // à un contexte (ou monade) dans lequel tout le calcul va se dérouler, - // et qui va pouvoir accumuler des informations au fur et à mesure, par - // exemple un cache des variables déjà calculées. - - // On a déjà vu un exemple de monade, c'était Writer: voyons comment on - // reformule le catamorphisme pour qu'il se déroule dans la monade Writer. - // L'implémentation de cataM est inspirée de - // https://github.com/DrBoolean/excursion/ - // D'abord on ajoute de la plomberie: - - const cataM = (of, algM) => m => - m - .project() - .traverse(of, x => x.cataM(of, algM)) - .chain(algM) - - const traverse = function(of, f) { - return this.cata({ - Num: x => of(this), - Add: (x, y) => f(x).chain(xx => f(y).chain(yy => of(Add(xx, yy)))), - Var: name => of(this) - }) - } - Expr.prototype.traverse = traverse - Fx.prototype.cataM = function(of, alg) { - return cataM(of, alg)(this) - } - - // Maintenant que c'est fait on voit qu'on a simplifié l'expression du - // catamorphisme: on n'a plus à expliciter l'enchaînement (sauf pour la - // récursion de plus haut niveau dans les variables) - - let trace2 = R.curry((rules, name) => { - let find = (rules, name) => - R.find(x => R.prop('name', x) == name, rules).expr, - expr = find(rules, name) - return cataM(Writer.of, tracer2(trace2(rules)))(expr) - }) - - const tracer2 = recurse => a => { - let log = (x, s) => Writer.tell(Str(s)).map(_ => x) - return a.cata({ - Num: x => log(x, x + ','), - Add: (x, y) => log(x + y, '+,'), - Var: name => recurse(name).chain(x => log(x, name + ',')) - }) - } - - it('should trace the shape of the computation, showing two passes through b', function() { - let rule1 = Assign('a', add(ref('b'), ref('c'))), - rule2 = Assign('b', num(15)), - rule3 = Assign('c', num(10)), - rules = [rule1, rule2, rule3], - result = trace2(rules, 'a').run(Str.zero) - expect(result.value).to.equal(25) - expect(result.output.s).to.equal('15,b,10,c,+,') - }) - - // On a la possibilité "d'encapsuler" une monade dans une autre: - // on va se doter d'un State, une monade qui permet de stocker un - // état et de le modifier en le propageant dans tout le calcul, et - // conserver Writer à l'intérieur (on utilise la variante StateT, - // le T veut dire "transformation de monade") - - // On peut aller plus loin et mémoiser le catamorphisme: - // https://idontgetoutmuch.wordpress.com/2011/05/15/monadic-caching-folds/ - // ça ne semble pas nécessaire ici puisque tout se passe au niveau de - // la récursion sur "Var" - - const S = StateT(Writer) - const log = (x, s) => S.lift(S.inner.tell(Str(s)).map(_ => x)) - - let trace3 = R.curry((rules, name) => { - let find = (rules, name) => - R.find(x => R.prop('name', x) == name, rules).expr, - expr = find(rules, name) - return cataM(S.of, tracer3(trace3(rules)))(expr) - }) - - const memoize = f => name => { - let cache = result => - result.chain(x => - result - .modify(state => R.assoc(name, run(result), state)) - .chain(z => S.of(x)) - ) - - return S.get.chain(state => { - let cached = state[name] - return cached ? S.of(cached.value.value) : cache(f(name)) - }) - } - - const tracer3 = recurse => a => { - return a.cata({ - Num: x => log(x, x + ','), - Add: (x, y) => log(x + y, '+,'), - Var: memoize(name => recurse(name).chain(x => log(x, name + ','))) - }) - } - - const run = (c, state) => Writer.run(StateT.run(c, state), Str.zero) - - it('should trace the shape of the computation, showing one pass through b', function() { - let rule1 = Assign('a', add(ref('b'), ref('b'))), - rule2 = Assign('b', num(15)), - rules = [rule1, rule2], - result = run(trace3(rules, 'a'), {}) - expect(result.value.value).to.equal(30) - expect(result.output.s).to.equal('15,b,+,') - }) -}) diff --git a/test/trees.md b/test/trees.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/variables.test.js b/test/variables.test.js index 3d1477f72..6ea35c146 100644 --- a/test/variables.test.js +++ b/test/variables.test.js @@ -10,22 +10,6 @@ describe('getSituationValue', function() { expect(getSituationValue(situationGate, 'salaire', rule)).to.equal('2300') }) - it("should interpret rules without a formula as boolean-valued, with 'oui' for true", function() { - let rule = {}, - state = { condition: 'oui' }, - situationGate = name => state[name] - - expect(getSituationValue(situationGate, 'condition', rule)).to.be.true - }) - - it("should interpret rules without a formula as boolean-valued, with 'non' meaning false", function() { - let rule = {}, - state = { condition: 'non' }, - situationGate = name => state[name] - - expect(getSituationValue(situationGate, 'condition', rule)).to.be.false - }) - it("should interpret rules with 'one of these', with 'oui' for true", function() { let rule = { formule: { 'une possibilité': ['noir', 'blanc'] } }, state = { condition: 'oui' },