From 8c7ab52a4fb599519129d9a025f2f71ce3206369 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Tue, 21 Apr 2020 15:49:48 +0200 Subject: [PATCH] =?UTF-8?q?Revert=20"Refacto=20:=20s=C3=A9paration=20clair?= =?UTF-8?q?e=20du=20moteur=20et=20de=20l'application=20=F0=9F=94=A5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 + circle.yml | 1 - componentTestSetup.js | 2 +- cypress/integration/mon-entreprise/covid19.js | 19 +- .../integration/mon-entreprise/simulateurs.js | 6 +- index.html | 2 +- source/Provider.tsx | 5 +- source/actions/actions.ts | 40 +- source/components/Banner.tsx | 2 +- source/components/Controls.tsx | 14 +- .../CurrencyInput/CurrencyInput.test.js | 4 +- .../CurrencyInput/CurrencyInput.tsx | 18 +- source/components/Distribution.tsx | 106 ++-- source/components/Documentation/Examples.js | 58 -- source/components/Documentation/Rule.js | 199 ------- .../Documentation/UseDefaultValuesContext.tsx | 3 - source/components/Documentation/index.tsx | 31 -- source/components/EngineValue.tsx | 65 --- source/components/Mecanisms.css | 7 + source/components/MoreInfosOnUs.tsx | 2 +- source/components/PaySlip.css | 10 - source/components/PaySlip.tsx | 175 +++--- source/components/PaySlipSections.tsx | 171 +++--- source/components/PercentageField.tsx | 5 +- source/components/PeriodSwitch.tsx | 4 +- .../components/PreviousSimulationBanner.tsx | 4 +- source/components/QuickLinks.tsx | 14 +- source/components/RuleLink.tsx | 10 +- source/components/RulePage.css | 7 + source/components/RulePage.tsx | 20 +- source/components/SalaryExplanation.tsx | 42 +- source/components/SchemeComparaison.tsx | 516 ++++++++++-------- source/components/SearchBar.tsx | 28 +- source/components/SearchButton.tsx | 6 +- source/components/SimulateurWarning.tsx | 2 +- source/components/Simulation.tsx | 6 +- source/components/TargetSelection.css | 4 +- source/components/TargetSelection.tsx | 205 +++---- source/components/Targets.css | 23 + source/components/Targets.tsx | 34 ++ source/components/Value.tsx | 85 +++ source/components/conversation/Aide.tsx | 12 +- source/components/conversation/AnswerList.tsx | 94 ++-- .../components/conversation/Conversation.tsx | 67 +-- source/components/conversation/Explicable.tsx | 4 +- .../components/conversation/FormDecorator.tsx | 15 +- source/components/conversation/GroupTitle.js | 43 ++ source/components/conversation/Input.js | 3 +- .../conversation/InputSuggestions.tsx | 27 +- source/components/conversation/Question.tsx | 2 +- .../conversation/SeeAnswersButton.tsx | 17 +- .../select/{SelectGeo.js => SelectGéo.js} | 18 +- .../{Documentation => rule}/Algorithm.css | 27 +- .../{Documentation => rule}/Algorithm.tsx | 44 +- .../{Documentation => rule}/Destinataire.css | 0 .../{Documentation => rule}/Destinataire.tsx | 0 source/components/rule/Examples.js | 79 +++ .../{Documentation => rule}/Header.css | 0 .../{Documentation => rule}/Header.js | 0 .../{Documentation => rule}/Namespace.css | 0 .../{Documentation => rule}/Namespace.tsx | 9 +- .../{Documentation => rule}/References.css | 16 +- .../{Documentation => rule}/References.tsx | 18 +- source/components/rule/Rule.css | 31 ++ source/components/rule/Rule.js | 239 ++++++++ .../{Documentation => rule}/RuleSource.tsx | 17 +- source/components/rule/ShowValuesContext.tsx | 9 + .../simulationConfigs/artiste-auteur.yaml | 2 +- .../simulationConfigs/assimilé.yaml | 2 +- .../simulationConfigs/auto-entrepreneur.yaml | 2 +- .../simulationConfigs/chômage-partiel.yaml | 5 +- .../simulationConfigs/indépendant.yaml | 2 +- .../rémunération-dirigeant.yaml | 22 +- .../components/simulationConfigs/salarié.yaml | 6 + source/components/ui/AnimatedTargetValue.tsx | 2 +- source/components/ui/Toggle.css | 2 +- source/components/ui/Typography.css | 2 +- source/components/ui/index.css | 5 - source/components/utils/EngineContext.tsx | 52 -- source/components/utils/useNextQuestion.tsx | 132 ----- ...SitePathsContext.tsx => withSitePaths.tsx} | 0 source/engine/RuleInput.tsx | 27 +- source/engine/error.ts | 4 +- source/engine/evaluateRule.ts | 37 +- source/engine/evaluation.tsx | 31 +- source/engine/format.test.js | 60 +- source/engine/format.ts | 50 +- source/engine/generateQuestions.ts | 60 ++ source/engine/getSituationValue.js | 10 + source/engine/index.ts | 164 ++---- source/engine/mecanismViews/Allègement.js | 2 +- source/engine/mecanismViews/Barème.css | 2 +- source/engine/mecanismViews/Barème.tsx | 5 +- source/engine/mecanismViews/Composantes.js | 2 +- source/engine/mecanismViews/Grille.tsx | 2 +- .../mecanismViews/InversionNumérique.js | 80 ++- source/engine/mecanismViews/Product.js | 2 +- source/engine/mecanismViews/Recalcul.tsx | 6 +- .../engine/mecanismViews/TauxProgressif.tsx | 5 +- source/engine/mecanismViews/Variations.js | 191 +++---- source/engine/mecanismViews/common.tsx | 95 ++-- source/engine/mecanisms.js | 298 +++++----- source/engine/mecanisms.yaml | 4 +- source/engine/mecanisms/arrondi.tsx | 32 +- source/engine/mecanisms/durée.tsx | 2 +- source/engine/mecanisms/encadrement.tsx | 6 +- source/engine/mecanisms/operation.js | 16 +- source/engine/mecanisms/régularisation.ts | 2 +- source/engine/mecanisms/trancheUtils.ts | 2 +- source/engine/mecanisms/variations.ts | 8 +- source/engine/nodeUnits.ts | 58 +- source/engine/parse.tsx | 24 +- source/engine/parseReference.js | 69 +-- source/engine/parseRule.tsx | 63 ++- source/engine/react.tsx | 53 ++ source/engine/ruleUtils.ts | 18 +- source/engine/temporal.ts | 35 +- source/engine/types.ts | 40 +- source/engine/units.ts | 42 +- source/locales/en.yaml | 4 +- source/locales/rules-en.yaml | 30 +- source/reducers/rootReducer.ts | 128 ++++- source/rules/artiste-auteur.yaml | 9 +- .../hôtels-cafés-restaurants.yaml | 2 +- .../conventions-collectives/optique.yaml | 3 +- .../spectacle-vivant.yaml | 5 +- .../rules/conventions-collectives/sport.yaml | 23 +- source/rules/dirigeant.yaml | 61 ++- .../rules/déclaration-revenu-indépendant.yaml | 4 +- source/rules/entreprise-établissement.yaml | 21 +- source/rules/impôt.yaml | 14 +- source/rules/protection-sociale.yaml | 20 +- source/rules/salarié.yaml | 192 ++++--- source/selectors/analyseSelectors.ts | 349 ++++++++++++ source/selectors/companyStatusSelectors.ts | 2 +- source/selectors/ficheDePaieSelectors.ts | 132 +++++ source/selectors/progressSelectors.ts | 8 + source/selectors/repartitionSelectors.ts | 88 +++ source/selectors/simulationSelectors.ts | 40 -- source/sites/mon-entreprise.fr/App.tsx | 124 ++--- .../layout/Footer/Footer.css | 2 +- .../layout/Footer/Footer.tsx | 2 +- .../sites/mon-entreprise.fr/layout/Header.tsx | 2 +- .../mon-entreprise.fr/layout/NewsBanner.tsx | 2 +- .../middlewares/trackSimulatorActions.ts | 28 +- .../mon-entreprise.fr/pages/Coronavirus.tsx | 54 +- .../pages/Créer/AfterRegistration.tsx | 2 +- .../pages/Créer/CreationChecklist.tsx | 2 +- .../Créer/GuideStatut/PickLegalStatus.tsx | 2 +- .../Créer/GuideStatut/PreviousAnswers.tsx | 2 +- .../pages/Créer/GuideStatut/index.tsx | 2 +- .../mon-entreprise.fr/pages/Créer/Home.tsx | 2 +- .../mon-entreprise.fr/pages/Créer/index.tsx | 2 +- .../mon-entreprise.fr/pages/Dev/Sitemap.tsx | 2 +- .../mon-entreprise.fr/pages/Documentation.tsx | 20 - .../pages}/Documentation/RulesList.css | 0 .../pages}/Documentation/RulesList.tsx | 6 +- .../pages/Documentation/index.tsx | 18 + .../AideDéclarationIndépendant/Result.tsx | 12 +- .../Récapitulatif.tsx | 120 ++++ .../AideDéclarationIndépendant/index.tsx | 33 +- .../mon-entreprise.fr/pages/Gérer/Home.tsx | 2 +- .../pages/Gérer/SchemeSelection.tsx | 2 +- .../mon-entreprise.fr/pages/Gérer/index.tsx | 37 +- .../pages/Iframes/SimulateurEmbauche.tsx | 2 +- .../pages/Landing/Landing.tsx | 2 +- .../pages/Nouveautés/Nouveautés.tsx | 2 +- .../pages/Simulateurs/ArtisteAuteur.tsx | 57 +- .../pages/Simulateurs/AutoEntrepreneur.tsx | 16 +- .../pages/Simulateurs/Home.tsx | 2 +- .../pages/Simulateurs/Indépendant.tsx | 14 +- .../pages/Simulateurs/Salarié.tsx | 4 +- .../pages/Simulateurs/index.tsx | 5 +- .../pages/integration/Options.tsx | 2 +- .../pages/integration/index.tsx | 2 +- .../pages/ÉconomieCollaborative/Activité.tsx | 42 +- .../ActivitésSelection.tsx | 2 +- .../ÉconomieCollaborative/NextButton.tsx | 2 +- .../ÉconomieCollaborative/VotreSituation.tsx | 2 +- .../pages/ÉconomieCollaborative/index.tsx | 2 +- source/sites/mon-entreprise.fr/sitePaths.ts | 4 + source/sites/publi.codes/Studio.tsx | 106 ++-- test/contrôles.test.js | 2 +- test/conversation.test.js | 152 +++++- test/ficheDePaieSelector.test.js | 70 +++ ...bles.test.js => generateQuestions.test.js} | 59 +- test/inversion.test.js | 20 +- test/library.test.js | 22 +- test/mecanisms.test.js | 13 +- test/mécanismes/allègement.yaml | 2 +- test/mécanismes/applicable.yaml | 10 +- test/mécanismes/conversion-unité.yaml | 52 +- test/mécanismes/date.yaml | 2 +- test/mécanismes/expressions.yaml | 25 +- test/mécanismes/le-maximum-de.yaml | 4 +- test/mécanismes/question-conditionelle.yaml | 2 +- test/mécanismes/remplace.yaml | 2 +- test/mécanismes/régularisation.yaml | 7 +- test/mécanismes/toutes-ces-conditions.yaml | 2 +- test/mécanismes/une-de-ces-conditions.yaml | 2 +- test/mécanismes/variable-temporelle.yaml | 7 +- test/mécanismes/variations.yaml | 8 +- test/real-rules.test.js | 9 +- .../__snapshots__/simulations.jest.js.snap | 452 +++++++-------- .../simulations-artiste-auteur.yaml | 14 +- .../simulations-auto-entrepreneur.yaml | 34 +- test/regressions/simulations-indépendant.yaml | 62 +-- .../simulations-rémunération-dirigeant.yaml | 62 +-- test/regressions/simulations-salarié.yaml | 294 +++++----- test/regressions/simulations.jest.js | 75 ++- test/rules/co2.yaml | 8 +- test/rules/sasu.yaml | 9 +- test/tree.test.js | 439 +++++++++++++++ test/trees.md | 0 test/variables.test.js | 16 + 215 files changed, 4982 insertions(+), 3430 deletions(-) delete mode 100644 source/components/Documentation/Examples.js delete mode 100644 source/components/Documentation/Rule.js delete mode 100644 source/components/Documentation/UseDefaultValuesContext.tsx delete mode 100644 source/components/Documentation/index.tsx delete mode 100644 source/components/EngineValue.tsx create mode 100644 source/components/Mecanisms.css create mode 100644 source/components/Targets.css create mode 100644 source/components/Targets.tsx create mode 100644 source/components/Value.tsx create mode 100644 source/components/conversation/GroupTitle.js rename source/components/conversation/select/{SelectGeo.js => SelectGéo.js} (94%) rename source/components/{Documentation => rule}/Algorithm.css (88%) rename source/components/{Documentation => rule}/Algorithm.tsx (64%) rename source/components/{Documentation => rule}/Destinataire.css (100%) rename source/components/{Documentation => rule}/Destinataire.tsx (100%) create mode 100644 source/components/rule/Examples.js rename source/components/{Documentation => rule}/Header.css (100%) rename source/components/{Documentation => rule}/Header.js (100%) rename source/components/{Documentation => rule}/Namespace.css (100%) rename source/components/{Documentation => rule}/Namespace.tsx (86%) rename source/components/{Documentation => rule}/References.css (72%) rename source/components/{Documentation => rule}/References.tsx (83%) create mode 100644 source/components/rule/Rule.css create mode 100644 source/components/rule/Rule.js rename source/components/{Documentation => rule}/RuleSource.tsx (54%) create mode 100644 source/components/rule/ShowValuesContext.tsx delete mode 100644 source/components/utils/EngineContext.tsx delete mode 100644 source/components/utils/useNextQuestion.tsx rename source/components/utils/{SitePathsContext.tsx => withSitePaths.tsx} (100%) create mode 100644 source/engine/generateQuestions.ts create mode 100644 source/engine/react.tsx create mode 100644 source/selectors/analyseSelectors.ts create mode 100644 source/selectors/ficheDePaieSelectors.ts create mode 100644 source/selectors/progressSelectors.ts create mode 100644 source/selectors/repartitionSelectors.ts delete mode 100644 source/selectors/simulationSelectors.ts delete mode 100644 source/sites/mon-entreprise.fr/pages/Documentation.tsx rename source/{components => sites/mon-entreprise.fr/pages}/Documentation/RulesList.css (100%) rename source/{components => sites/mon-entreprise.fr/pages}/Documentation/RulesList.tsx (70%) create mode 100644 source/sites/mon-entreprise.fr/pages/Documentation/index.tsx create mode 100644 source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx create mode 100644 test/ficheDePaieSelector.test.js rename test/{missingVariables.test.js => generateQuestions.test.js} (82%) create mode 100644 test/tree.test.js create mode 100644 test/trees.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 6ebc57b1f..36cdf4ec8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,9 @@ "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 bed49caf9..778177bf2 100644 --- a/circle.yml +++ b/circle.yml @@ -50,7 +50,6 @@ jobs: steps: - install - run: | - git config --global core.quotepath false yarn test yarn test-regressions diff --git a/componentTestSetup.js b/componentTestSetup.js index 45fd41835..b328984b1 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 478f21ee5..9826df5d4 100644 --- a/cypress/integration/mon-entreprise/covid19.js +++ b/cypress/integration/mon-entreprise/covid19.js @@ -1,13 +1,4 @@ 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) { @@ -20,12 +11,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() - testText('comparaison-net', 'Soit 100 % du revenu net') - testText('comparaison-total', 'Soit 0 % du coût habituel') + cy.contains('Soit 100% du revenu net') + cy.contains('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() - testText('comparaison-net', 'Soit 85 % du revenu net') - testText('comparaison-total', 'Soit 0 % du coût habituel') + cy.contains('Soit 85% du revenu net') + cy.contains('Soit 0% du coût habituel') }) }) diff --git a/cypress/integration/mon-entreprise/simulateurs.js b/cypress/integration/mon-entreprise/simulateurs.js index 81b51e821..cb49daca4 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(800) + cy.wait(600) 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(800) + cy.wait(600) cy.contains('€ / mois').click() cy.get(inputSelector) .first() @@ -108,7 +108,7 @@ describe('Simulateurs', function() { cy.get(inputSelector) .first() .type('{selectall}5000') - cy.wait(800) + cy.wait(600) 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 0d0a62b0b..612f2fc07 100644 --- a/index.html +++ b/index.html @@ -62,7 +62,7 @@ /> diff --git a/source/Provider.tsx b/source/Provider.tsx index acbaf27ee..30cdab2b0 100644 --- a/source/Provider.tsx +++ b/source/Provider.tsx @@ -1,8 +1,6 @@ import { ThemeColorsProvider } from 'Components/utils/colors' -import { EngineProvider } from 'Components/utils/EngineContext' -import { SitePathProvider, SitePaths } from 'Components/utils/SitePathsContext' +import { SitePathProvider, SitePaths } from 'Components/utils/withSitePaths' import { TrackerProvider } from 'Components/utils/withTracker' -import Engine from 'Engine' import { createBrowserHistory } from 'history' import { AvailableLangs } from 'i18n' import i18next from 'i18next' @@ -14,7 +12,6 @@ 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 041ea58e5..939ff6ea1 100644 --- a/source/actions/actions.ts +++ b/source/actions/actions.ts @@ -1,4 +1,4 @@ -import { SitePaths } from 'Components/utils/SitePathsContext' +import { SitePaths } from 'Components/utils/withSitePaths' import { History } from 'history' import { RootState, SimulationConfig } from 'Reducers/rootReducer' import { ThunkAction } from 'redux-thunk' @@ -12,12 +12,13 @@ export type Action = | UpdateAction | SetSimulationConfigAction | DeletePreviousSimulationAction + | SetExempleAction | ExplainVariableAction | UpdateSituationAction | HideControlAction | LoadPreviousSimulationAction | SetSituationBranchAction - | UpdateTargetUnitAction + | UpdateDefaultUnitAction | SetActiveTargetAction | CompanyStatusAction @@ -45,6 +46,18 @@ 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 @@ -53,14 +66,14 @@ type SetSituationBranchAction = ReturnType type SetActiveTargetAction = ReturnType type HideControlAction = ReturnType type ExplainVariableAction = ReturnType -type UpdateTargetUnitAction = ReturnType +type UpdateDefaultUnitAction = ReturnType export const resetSimulation = () => ({ type: 'RESET_SIMULATION' } as const) -export const goToQuestion = (question: DottedName) => +export const goToQuestion = (question: string) => ({ type: 'STEP_ACTION', name: 'unfold', @@ -86,7 +99,7 @@ export const setSituationBranch = (id: number) => } as const) export const setSimulationConfig = ( - config: SimulationConfig, + config: Object, useCompanyDetails: boolean = false ): ThunkResult => (dispatch, getState, { history }): void => { if (getState().simulation?.config === config) { @@ -121,17 +134,26 @@ export const updateSituation = (fieldName: DottedName, value: unknown) => value } as const) -export const updateUnit = (targetUnit: string) => +export const updateUnit = (defaultUnit: string) => ({ - type: 'UPDATE_TARGET_UNIT', - targetUnit + type: 'UPDATE_DEFAULT_UNIT', + defaultUnit } 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 881e549ad..d751ae5d1 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/simulationSelectors' +import { firstStepCompletedSelector } from 'Selectors/analyseSelectors' import Animate from 'Ui/animate' import './Banner.css' diff --git a/source/components/Controls.tsx b/source/components/Controls.tsx index 087c2580a..a55a2da3c 100644 --- a/source/components/Controls.tsx +++ b/source/components/Controls.tsx @@ -1,22 +1,24 @@ 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 answeredQuestions = useSelector(answeredQuestionsSelector) - const controls = useControls() - const inversionFail = useInversionFail() + const foldedSteps = useSelector( + (state: RootState) => state.simulation?.foldedSteps + ) + const analysis = useSelector(analysisWithDefaultsSelector) + const controls = analysis?.controls + const inversionFail = analysis?.cache._meta.inversionFail const hiddenControls = useSelector( (state: RootState) => state.simulation?.hiddenControls ) @@ -54,7 +56,7 @@ export default function Controls() { {makeJsx(evaluated)} )} - {solution && !answeredQuestions?.includes(solution.cible) && ( + {solution && !foldedSteps?.includes(solution.cible) && (
- )} - - ) -} - -let Example = ({ - ex: { nom, situation }, - rule, - currentExample, - setCurrentExample -}) => { - let selected = currentExample && currentExample.name == nom - return ( -
  • - -
  • - ) -} diff --git a/source/components/Documentation/Rule.js b/source/components/Documentation/Rule.js deleted file mode 100644 index 38ba99d14..000000000 --- a/source/components/Documentation/Rule.js +++ /dev/null @@ -1,199 +0,0 @@ -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/Documentation/UseDefaultValuesContext.tsx b/source/components/Documentation/UseDefaultValuesContext.tsx deleted file mode 100644 index 92ae87256..000000000 --- a/source/components/Documentation/UseDefaultValuesContext.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react' - -export const UseDefaultValuesContext = createContext(true) diff --git a/source/components/Documentation/index.tsx b/source/components/Documentation/index.tsx deleted file mode 100644 index d2e467fc2..000000000 --- a/source/components/Documentation/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 0cbb3a196..000000000 --- a/source/components/EngineValue.tsx +++ /dev/null @@ -1,65 +0,0 @@ -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 new file mode 100644 index 000000000..734d125f9 --- /dev/null +++ b/source/components/Mecanisms.css @@ -0,0 +1,7 @@ +ul#mecanisms { + margin: 3em auto; +} + +#mecanisms .warning { + color: #e74c3c; +} diff --git a/source/components/MoreInfosOnUs.tsx b/source/components/MoreInfosOnUs.tsx index 20ba39bf6..8ff719af2 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/SitePathsContext' +import { SitePathsContext } from './utils/withSitePaths' export default function MoreInfosOnUs() { const { pathname } = useLocation() diff --git a/source/components/PaySlip.css b/source/components/PaySlip.css index c8d370dfe..2e2bef903 100644 --- a/source/components/PaySlip.css +++ b/source/components/PaySlip.css @@ -31,9 +31,6 @@ background-color: transparent !important; } -.payslip__container h5 { - margin: 0; -} .payslip__container h5 { margin: 0; } @@ -44,13 +41,6 @@ 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 de786014c..f944e8079 100644 --- a/source/components/PaySlip.tsx +++ b/source/components/PaySlip.tsx @@ -1,66 +1,28 @@ -import { useEvaluation, EngineContext } from 'Components/utils/EngineContext' -import Value from 'Components/EngineValue' -import { formatValue } from 'Engine/format' +import { ThemeColorsContext } from 'Components/utils/colors' +import Value from 'Components/Value' +import { getRuleFromAnalysis } from 'Engine/ruleUtils' import React, { Fragment, useContext } from 'react' import { Trans } from 'react-i18next' -import { DottedName } from 'Rules' +import { useSelector } from 'react-redux' +import { + analysisWithDefaultsSelector, + parsedRulesSelector +} from 'Selectors/analyseSelectors' +import { analysisToCotisationsSelector } from 'Selectors/ficheDePaieSelectors' import './PaySlip.css' import { Line, SalaireBrutSection, SalaireNetSection } from './PaySlipSections' import RuleLink from './RuleLink' -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 - -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) + const { lightestColor } = useContext(ThemeColorsContext) + const cotisations = useSelector(analysisToCotisationsSelector) + const analysis = useSelector(analysisWithDefaultsSelector) + const parsedRules = useSelector(parsedRulesSelector) + let getRule = getRuleFromAnalysis(analysis) + + const heuresSupplémentaires = getRule( + 'contrat salarié . temps de travail . heures supplémentaires' + ) return (
    - + {!!heuresSupplémentaires?.nodeValue && ( + + )}
    - + {/* Section cotisations */}

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

    Part salarié

    - {cotisationsBySection.map(([sectionDottedName, cotisations]) => { - let section = parsedRules[sectionDottedName] + {cotisations.map(([brancheDottedName, cotisationList]) => { + let branche = parsedRules[brancheDottedName] return ( - +
    - +
    - {cotisations.map(cotisation => ( - + {cotisationList.map(cotisation => ( + + + + {cotisation.montant.partPatronale} + + + {cotisation.montant.partSalariale} + + ))}
    ) @@ -118,57 +101,23 @@ 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 cd8123fcb..cc5b577b3 100644 --- a/source/components/PaySlipSections.tsx +++ b/source/components/PaySlipSections.tsx @@ -1,94 +1,125 @@ -import { EngineContext } from 'Components/utils/EngineContext' -import Value, { ValueProps, Condition } from 'Components/EngineValue' -import React, { useContext } from 'react' +import Value from 'Components/Value' +import { EvaluatedRule } from 'Engine/types' +import React from 'react' import { Trans } from 'react-i18next' +import { useSelector } from 'react-redux' import { DottedName } from 'Rules' -import { coerceArray } from '../utils' +import { defaultUnitSelector } from 'Selectors/analyseSelectors' import RuleLink from './RuleLink' -export let SalaireBrutSection = () => { +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') 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 SalaireNetSection = () => { +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' + ) 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 0333bcb5a..36cb92eac 100644 --- a/source/components/PercentageField.tsx +++ b/source/components/PercentageField.tsx @@ -1,6 +1,5 @@ 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' @@ -10,7 +9,6 @@ export default function PercentageField({ onChange, value, debounce = 0 }) { debounce ? debounceFn(debounce, onChange) : onChange, [debounce, onChange] ) - const language = useTranslation().i18n.language return (
    @@ -30,8 +28,7 @@ export default function PercentageField({ onChange, value, debounce = 0 }) { /> {formatValue({ - nodeValue: localValue, - language, + value: localValue, unit: '%' })} diff --git a/source/components/PeriodSwitch.tsx b/source/components/PeriodSwitch.tsx index 73bc010bd..a42b7be43 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 { targetUnitSelector } from 'Selectors/simulationSelectors' +import { defaultUnitSelector } from 'Selectors/analyseSelectors' import './PeriodSwitch.css' export default function PeriodSwitch() { const dispatch = useDispatch() const language = useTranslation().i18n.language - const currentUnit = useSelector(targetUnitSelector) + const currentUnit = useSelector(defaultUnitSelector) let units = ['€/mois', '€/an'] return ( diff --git a/source/components/PreviousSimulationBanner.tsx b/source/components/PreviousSimulationBanner.tsx index 7544b3fc2..93445c06e 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(firstStepCompletedSelector) + const newSimulationStarted = !useSelector(noUserInputSelector) const dispatch = useDispatch() return ( diff --git a/source/components/QuickLinks.tsx b/source/components/QuickLinks.tsx index 246863f7a..5a22aa4f8 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 { - answeredQuestionsSelector, - currentQuestionSelector -} from 'Selectors/simulationSelectors' + currentQuestionSelector, + nextStepsSelector +} from 'Selectors/analyseSelectors' export default function QuickLinks() { const currentQuestion = useSelector(currentQuestionSelector) - const nextSteps = useNextQuestions() + const nextSteps = useSelector(nextStepsSelector) const quickLinks = useSelector( (state: RootState) => state.simulation?.config.questions?.["à l'affiche"] ) - const quickLinksToHide = useSelector(answeredQuestionsSelector) + const quickLinksToHide = useSelector( + (state: RootState) => state.simulation?.foldedSteps || [] + ) const dispatch = useDispatch() if (!quickLinks) { diff --git a/source/components/RuleLink.tsx b/source/components/RuleLink.tsx index e6738a13b..0493b100d 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' import { nameLeaf } from 'Engine/ruleUtils' import { ParsedRule } from 'Engine/types' import React, { useContext } from 'react' @@ -25,14 +25,10 @@ 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 99f04887e..94ce70af2 100644 --- a/source/components/SalaryExplanation.tsx +++ b/source/components/SalaryExplanation.tsx @@ -2,20 +2,29 @@ import Distribution from 'Components/Distribution' import PaySlip from 'Components/PaySlip' import StackedBarChart from 'Components/StackedBarChart' import { ThemeColorsContext } from 'Components/utils/colors' -import { useEvaluation, useInversionFail } from 'Components/utils/EngineContext' +import { getRuleFromAnalysis } from 'Engine/ruleUtils' 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(answeredQuestionsSelector).length + const showDistributionFirst = useSelector( + (state: RootState) => !state.simulation?.foldedSteps.length + ) + const analysis = useSelector(analysisWithDefaultsSelector) + const inversionFail = analysis?.cache._meta.inversionFail const distributionRef = useRef(null) - if (useInversionFail()) { + // We can't provide an explanation if the engine has failed to run the + // simulation. + if (inversionFail) { return null } return ( @@ -74,16 +83,11 @@ 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 (

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

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

    diff --git a/source/components/SchemeComparaison.tsx b/source/components/SchemeComparaison.tsx index d5fff8de8..de60c8dd7 100644 --- a/source/components/SchemeComparaison.tsx +++ b/source/components/SchemeComparaison.tsx @@ -1,4 +1,4 @@ -import { setSimulationConfig } from 'Actions/actions' +import { setSimulationConfig, setSituationBranch } from 'Actions/actions' import { defineDirectorStatus, isAutoentrepreneur @@ -6,24 +6,34 @@ import { import classnames from 'classnames' import Conversation from 'Components/conversation/Conversation' import SeeAnswersButton from 'Components/conversation/SeeAnswersButton' -import Value from 'Components/EngineValue' -import dirigeantComparaison from 'Components/simulationConfigs/rémunération-dirigeant.yaml' -import Engine from 'Engine' +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 revenusSVG from 'Images/revenus.svg' -import { - default as React, - useCallback, - useContext, - useMemo, - useState -} from 'react' +import { default as React, useCallback, useContext, useState } from 'react' import emoji from 'react-easy-emoji' import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { situationSelector } from 'Selectors/simulationSelectors' +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 InfoBulle from 'Ui/InfoBulle' import './SchemeComparaison.css' -import { EngineContext } from './utils/EngineContext' + +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) +} type SchemeComparaisonProps = { hideAutoEntrepreneur?: boolean @@ -35,50 +45,27 @@ export default function SchemeComparaison({ hideAssimiléSalarié = false }: SchemeComparaisonProps) { const dispatch = useDispatch() - dispatch(setSimulationConfig(dirigeantComparaison)) - const plafondAutoEntrepreneurDépassé = useContext(EngineContext) - .controls() - .find( + dispatch(setSimulationConfig(ComparaisonConfig)) + + const analyses = useSelector(analysisWithDefaultsSelector) + const plafondAutoEntrepreneurDépassé = useSelector((state: RootState) => + branchAnalyseSelector(state, { + situationBranchName: 'Auto-entrepreneur' + }).controls?.find( ({ test }) => test.includes && test.includes('base des cotisations > plafond') ) + ) + let getRule = getRuleFrom(analyses) const [showMore, setShowMore] = useState(false) const [conversationStarted, setConversationStarted] = useState( - !!Object.keys(useSelector(situationSelector)).length + !!getRule('assimilé', 'revenu net après impôt')?.nodeValue ) 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 ? ( <> @@ -328,185 +325,244 @@ export default function SchemeComparaison({
    )}
    - {displayResult && ( - <> - -

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

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

    - - Pension de retraite - (avant impôts) + {conversationStarted && + !!getRule('assimilé', 'revenu net après impôt')?.nodeValue && ( + <> + +

    Revenu net après impôt

    -

    -
    - {' '} - - - Pension calculée pour 172 trimestres cotisés au régime général - sans variations de revenus. +
    + + + +
    +
    + + + +
    +
    + + {plafondAutoEntrepreneurDépassé ? ( + 'Plafond de CA dépassé' + ) : ( + + )} + +
    + +

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

    +
    +
    + +
    +
    + +
    +
    + {plafondAutoEntrepreneurDépassé ? ( + '—' + ) : ( + + )} +
    +

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

    -
    - {' '} - - - 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 en - auto-entrepreneur sans variations de revenus. + + Pension calculée pour 172 trimestres cotisés au régime + général sans variations de revenus. - - )} -
    - -

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

    -
    -
    - -
    -
    - -
    -
    - {plafondAutoEntrepreneurDépassé ? ( - '—' - ) : ( - +
    +
    + {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} /> - )} -
    - -

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

    -
    -
    - - +
    + trimestres} + unit={null} /> - - - ( - {' '} - - pour les accidents de trajet/travail et maladie pro - - ) - -
    -
    - -
    -
    - {plafondAutoEntrepreneurDépassé ? ( - '—' - ) : ( +
    +
    + {plafondAutoEntrepreneurDépassé ? ( + '—' + ) : ( + trimestres} + unit={null} + /> + )} +
    + +

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

    +
    +
    - + / 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 + + } + /> + + )} +
    + + )}

    @@ -566,3 +622,35 @@ 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 eef6d5ec7..1ecda922f 100644 --- a/source/components/SearchBar.tsx +++ b/source/components/SearchBar.tsx @@ -1,4 +1,4 @@ -import { SitePathsContext } from 'Components/utils/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' import { parentName } from 'Engine/ruleUtils' import { ParsedRule, ParsedRules } from 'Engine/types' import { pick, sortBy, take } from 'ramda' @@ -10,7 +10,6 @@ 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() @@ -35,16 +34,15 @@ 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({ - pathname: sitePaths.documentation.rule( + history.push( + sitePaths.documentation.rule( results[focusElem > 0 ? focusElem : 0].dottedName - ), - state: { useDefaultValues } - }) + ) + ) } if ( @@ -193,12 +191,7 @@ export default function SearchBar({ return ( <> {formattedResults.length === 0 && ( - + {title || capitalise0(name) || ''} )} @@ -209,12 +202,7 @@ export default function SearchBar({ return ( diff --git a/source/components/SearchButton.tsx b/source/components/SearchButton.tsx index 507ad9a51..8e91b1d03 100644 --- a/source/components/SearchButton.tsx +++ b/source/components/SearchButton.tsx @@ -1,9 +1,9 @@ -import React, { useEffect, useState, useContext } from 'react' +import React, { useEffect, useState } 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 = useContext(EngineContext).getParsedRules() + const rules = useSelector(parsedRulesSelector) const [visible, setVisible] = useState(false) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { diff --git a/source/components/SimulateurWarning.tsx b/source/components/SimulateurWarning.tsx index 6c97ce241..828304df6 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/SitePathsContext' +import { SitePaths } from './utils/withSitePaths' type SimulateurWarningProps = { simulateur: Exclude diff --git a/source/components/Simulation.tsx b/source/components/Simulation.tsx index 937fb6a11..ed7794e37 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/simulationSelectors' -import { useSimulationProgress } from 'Components/utils/useNextQuestion' +import { firstStepCompletedSelector } from 'Selectors/analyseSelectors' +import { simulationProgressSelector } from 'Selectors/progressSelectors' 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 = useSimulationProgress() + const progress = useSelector(simulationProgressSelector) return ( <> diff --git a/source/components/TargetSelection.css b/source/components/TargetSelection.css index ba864b014..282a9ed1a 100644 --- a/source/components/TargetSelection.css +++ b/source/components/TargetSelection.css @@ -62,7 +62,7 @@ #targetSelection .optionTitle { font-size: 115%; - font-weight: 600; + font-weight: 500; } #targetSelection .optionTitle a { color: inherit; @@ -169,7 +169,7 @@ font-style: italic; color: #c0392b; background: yellow; - font-weight: 600; + font-weight: 500; } /* 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 8d19b9534..5d8fca48f 100644 --- a/source/components/TargetSelection.tsx +++ b/source/components/TargetSelection.tsx @@ -3,16 +3,11 @@ import InputSuggestions from 'Components/conversation/InputSuggestions' import PeriodSwitch from 'Components/PeriodSwitch' import RuleLink from 'Components/RuleLink' import { ThemeColorsContext } from 'Components/utils/colors' -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 { 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 emoji from 'react-easy-emoji' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -20,9 +15,10 @@ import { Link, useLocation } from 'react-router-dom' import { RootState } from 'Reducers/rootReducer' import { DottedName } from 'Rules' import { + analysisWithDefaultsSelector, situationSelector, - targetUnitSelector -} from 'Selectors/simulationSelectors' + useTarget +} from 'Selectors/analyseSelectors' import Animate from 'Ui/animate' import AnimatedTargetValue from 'Ui/AnimatedTargetValue' import CurrencyInput from './CurrencyInput/CurrencyInput' @@ -30,12 +26,48 @@ 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 }, []) @@ -45,7 +77,7 @@ export default function TargetSelection({ showPeriodSwitch = true }) { {((typeof objectifs[0] === 'string' ? [{ objectifs }] : objectifs) as any).map( - ({ icône, objectifs: targets, nom }, index) => ( + ({ icône, objectifs: groupTargets, nom }, index) => (
    @@ -69,16 +101,14 @@ export default function TargetSelection({ showPeriodSwitch = true }) { )` }} > -
      - {' '} - {targets.map(target => ( - - ))} -
    + + groupTargets.includes(dottedName) + ), + initialRender + }} + /> ) @@ -87,25 +117,36 @@ export default function TargetSelection({ showPeriodSwitch = true }) { ) } -type TargetProps = { - dottedName: DottedName - initialRender: boolean -} -const Target = ({ dottedName, initialRender }: TargetProps) => { +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 }) => { const activeInput = useSelector((state: RootState) => state.activeTargetInput) const dispatch = useDispatch() - 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 + const isActiveInput = activeInput === target.dottedName + const isSmallTarget = !!target.question !== !!target.formule return (
  • { onFirstClick={value => { dispatch(updateSituation(target.dottedName, value)) }} - unit={target.unit} + unit={target.defaultUnit} />
  • @@ -168,12 +209,7 @@ let Header = ({ target }) => { - + {target.title || target.name} {hackyShowPeriod && ' ' + t('mensuel')} @@ -185,7 +221,7 @@ let Header = ({ target }) => { } type TargetInputOrValueProps = { - target: EvaluatedRule + target: ParsedRule isActiveInput: boolean isSmallTarget: boolean } @@ -199,29 +235,16 @@ function TargetInputOrValue({ const colors = useContext(ThemeColorsContext) const dispatch = useDispatch() const situationValue = useSelector(situationSelector)[target.dottedName] - const targetUnit = useSelector(targetUnitSelector) - const engine = useContext(EngineContext) + + const targetWithValue = useTarget(target.dottedName) + const inversionFail = useSelector(analysisWithDefaultsSelector)?.cache._meta + .inversionFail const value = - typeof situationValue === 'string' - ? Math.round( - engine.evaluate(situationValue, { unit: targetUnit }) - .nodeValue as number - ) - : situationValue != null - ? situationValue - : target?.nodeValue != null - ? Math.round(+target.nodeValue) + targetWithValue?.nodeValue != null && !inversionFail + ? Math.round(targetWithValue.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)) + ) } - onChange={onChange} onFocus={() => { if (isSmallTarget) return dispatch(setActiveTarget(target.dottedName)) @@ -269,10 +292,8 @@ function TargetInputOrValue({ ) } function TitreRestaurant() { - const targetUnit = useSelector(targetUnitSelector) - const titresRestaurant = useEvaluation( - 'contrat salarié . frais professionnels . titres-restaurant . montant', - { unit: targetUnit } + const titresRestaurant = useTarget( + 'contrat salarié . frais professionnels . titres-restaurant . montant' ) const { language } = useTranslation().i18n if (!titresRestaurant?.nodeValue) return null @@ -282,11 +303,7 @@ function TitreRestaurant() { +{' '} - {formatValue({ - nodeValue: titresRestaurant.nodeValue, - unit: '€', - language - })} + {formatCurrency(titresRestaurant.nodeValue, language)} {' '} en titres-restaurant {emoji(' 🍽')} @@ -295,18 +312,16 @@ function TitreRestaurant() { ) } function AidesGlimpse() { - const targetUnit = useSelector(targetUnitSelector) - const aides = useEvaluation('contrat salarié . aides employeur', { - unit: targetUnit - }) + const aides = useTarget('contrat salarié . aides employeur') 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 aidesDetail = aides?.formule.explanation.explanation + const aidesNode = aides?.explanation + const aidesDetail = aides?.explanation.formule.explanation.explanation const aidesNotNul = aidesDetail?.filter(node => node.nodeValue !== false) - const aideLink = aidesNotNul?.length === 1 ? aidesNotNul[0] : aides + const aideLink = aidesNotNul?.length === 1 ? aidesNotNul[0] : aidesNode if (!aides?.nodeValue) return null return ( @@ -315,15 +330,11 @@ function AidesGlimpse() { en incluant{' '} - - {formatValue({ - nodeValue: aides.nodeValue, - unit: '€', - language - })} - + + {formatCurrency(aides.nodeValue, language)} + {' '} - d'aides {emoji(aides?.icons ?? '')} + d'aides {emoji(aides.explanation?.icons ?? '')}
    diff --git a/source/components/Targets.css b/source/components/Targets.css new file mode 100644 index 000000000..4f479c1d8 --- /dev/null +++ b/source/components/Targets.css @@ -0,0 +1,23 @@ +#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 new file mode 100644 index 000000000..3b6f183d6 --- /dev/null +++ b/source/components/Targets.tsx @@ -0,0 +1,34 @@ +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 new file mode 100644 index 000000000..515a35ac7 --- /dev/null +++ b/source/components/Value.tsx @@ -0,0 +1,85 @@ +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 29f8a664e..aedf52664 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, { useContext } from 'react' +import React from 'react' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' -import References from '../Documentation/References' +import { parsedRulesSelector } from 'Selectors/analyseSelectors' +import References from '../rule/References' import './Aide.css' -import { EngineContext } from 'Components/utils/EngineContext' export default function Aide() { const explained = useSelector((state: RootState) => state.explainedVariable) - const rules = useContext(EngineContext).getParsedRules() + const rules = useSelector(parsedRulesSelector) const dispatch = useDispatch() const stopExplaining = () => dispatch(explainVariable()) @@ -29,7 +29,9 @@ export default function Aide() { `} >

    {rule.title}

    - +

    + +

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

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

    - - - )} - {!!nextSteps.length && ( +

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

    + + {next.length > 0 && ( <>

    {emoji('🔮 ')} Prochaines questions

    - + )}
    ) } -function StepsTable({ - rules, - onClose -}: { - rules: Array - onClose: () => void -}) { +function StepsTable({ rules, onClose }) { const dispatch = useDispatch() - const evaluatedRules = useEvaluation(rules) - const language = useTranslation().i18n.language return ( - {evaluatedRules - .filter(rule => rule.isApplicable !== false) + {rules + .filter(rule => rule.nodeValue !== undefined) .map(rule => ( span { border-bottom-color: var(--textColorOnWhite); padding: 0.05em 0em; @@ -108,7 +98,7 @@ function StepsTable({ `} > - {formatValue({ ...rule, language })} + {' '} @@ -118,3 +108,17 @@ function StepsTable({
    ) } + +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 9e36f1016..ef5f8adfc 100644 --- a/source/components/conversation/Conversation.tsx +++ b/source/components/conversation/Conversation.tsx @@ -1,22 +1,20 @@ import { goToQuestion, validateStepWithValue } from 'Actions/actions' import QuickLinks from 'Components/QuickLinks' import RuleInput from 'Engine/RuleInput' -import React, { useContext, useEffect } from 'react' +import React 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 @@ -24,15 +22,18 @@ export type ConversationProps = { export default function Conversation({ customEndMessages }: ConversationProps) { const dispatch = useDispatch() - const rules = useContext(EngineContext).getParsedRules() - const currentQuestion = useNextQuestions()[0] + const rules = useSelector(parsedRulesSelector) + const currentQuestion = useSelector(currentQuestionSelector) + const previousAnswers = useSelector( + (state: RootState) => state.simulation?.foldedSteps || [] + ) + const nextSteps = useSelector(nextStepsSelector) - const previousAnswers = useSelector(answeredQuestionsSelector) const setDefault = () => dispatch( validateStepWithValue( currentQuestion, - rules[currentQuestion]['par défaut'] + rules[currentQuestion].defaultValue ) ) const goToPrevious = () => @@ -44,31 +45,35 @@ export default function Conversation({ customEndMessages }: ConversationProps) { } const DecoratedInputComponent = FormDecorator(RuleInput) - return currentQuestion ? ( + return rules && nextSteps.length ? ( <>
    - - - -
    - {previousAnswers.length > 0 && ( - <> + {currentQuestion && ( + + + + +
    + {previousAnswers.length > 0 && ( + <> + + + )} - - )} - -
    +
    + + )}
    diff --git a/source/components/conversation/Explicable.tsx b/source/components/conversation/Explicable.tsx index 81ad54af1..b614f2cb5 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 d7deb35b3..4137fb5ea 100644 --- a/source/components/conversation/FormDecorator.tsx +++ b/source/components/conversation/FormDecorator.tsx @@ -1,9 +1,12 @@ -import { updateSituation, goToQuestion } from 'Actions/actions' +import { updateSituation } from 'Actions/actions' import Explicable from 'Components/conversation/Explicable' -import React, { useContext } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { situationSelector } from 'Selectors/simulationSelectors' -import { EngineContext } from 'Components/utils/EngineContext' +import { + parsedRulesSelector, + situationSelector +} from 'Selectors/analyseSelectors' /* This higher order component wraps "Form" components (e.g. Question.js), that represent user inputs, @@ -17,8 +20,9 @@ export default function FormDecorator(RenderField) { return function FormStep({ dottedName }) { const dispatch = useDispatch() const situation = useSelector(situationSelector) - const rules = useContext(EngineContext).getParsedRules() + const rules = useSelector(parsedRulesSelector) + const language = useTranslation().i18n.language const submit = source => dispatch({ type: 'STEP_ACTION', @@ -27,7 +31,6 @@ 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 new file mode 100644 index 000000000..32fdd41f7 --- /dev/null +++ b/source/components/conversation/GroupTitle.js @@ -0,0 +1,43 @@ +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 64a846358..b515c0f15 100644 --- a/source/components/conversation/Input.js +++ b/source/components/conversation/Input.js @@ -33,7 +33,6 @@ export default function Input({ onChange(value) }} onSecondClick={() => onSubmit && onSubmit('suggestion')} - unit={unit} />
    @@ -42,7 +41,7 @@ export default function Input({ autoFocus={autoFocus} className="suffixed" id={'step-' + dottedName} - placeholder={defaultValue?.nodeValue ?? defaultValue} + placeholder={defaultValue} thousandSeparator={thousandSeparator} decimalSeparator={decimalSeparator} allowEmptyFormatting={true} diff --git a/source/components/conversation/InputSuggestions.tsx b/source/components/conversation/InputSuggestions.tsx index 75290bdcc..f0e949e61 100644 --- a/source/components/conversation/InputSuggestions.tsx +++ b/source/components/conversation/InputSuggestions.tsx @@ -1,12 +1,15 @@ +import { Rule } from 'Engine/types' import { toPairs } from 'ramda' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import { serializeUnit, Unit } from '../../engine/units' +import { useSelector } from 'react-redux' +import { defaultUnitSelector } from 'Selectors/analyseSelectors' +import { convertUnit, parseUnit, Unit } from '../../engine/units' type InputSuggestionsProps = { - suggestions?: Record - onFirstClick: (val: string) => void - onSecondClick?: (val: string) => void + suggestions?: Rule['suggestions'] + onFirstClick: (val: number | string) => void + onSecondClick?: (val: number | string) => void unit?: Unit } @@ -18,25 +21,27 @@ 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, number]) => { - const valueWithUnit: string = `${value} ${ - unit ? serializeUnit(unit)?.replace(' / ', '/') : '' - }` + {toPairs(suggestions).map(([text, value]: [string, string | number]) => { + value = + unit && typeof value === 'number' + ? convertUnit(unit, defaultUnit, value) + : value return ( + {arePreviousAnswers && ( + + )} {showAnswerModal && setShowAnswerModal(false)} />} ) diff --git a/source/components/conversation/select/SelectGeo.js b/source/components/conversation/select/SelectGéo.js similarity index 94% rename from source/components/conversation/select/SelectGeo.js rename to source/components/conversation/select/SelectGéo.js index 1232d1d78..177cc0762 100644 --- a/source/components/conversation/select/SelectGeo.js +++ b/source/components/conversation/select/SelectGéo.js @@ -47,14 +47,16 @@ export default function Select({ onChange, onSubmit }) { tauxVersementTransport(option.code) .then(({ taux }) => { // serialize to not mix our data schema and the API response's - onChange({ - ...option, - ...(taux != undefined - ? { - 'taux du versement transport': taux - } - : {}) - }) + onChange( + JSON.stringify({ + ...option, + ...(taux != undefined + ? { + 'taux du versement transport': taux + } + : {}) + }) + ) onSubmit() }) .catch(error => { diff --git a/source/components/Documentation/Algorithm.css b/source/components/rule/Algorithm.css similarity index 88% rename from source/components/Documentation/Algorithm.css rename to source/components/rule/Algorithm.css index 4eab61bdc..dab74c846 100644 --- a/source/components/Documentation/Algorithm.css +++ b/source/components/rule/Algorithm.css @@ -3,6 +3,7 @@ */ .situationValue { + display: none; text-decoration: underline white; border-bottom-left-radius: 3px; font-weight: 400; @@ -13,6 +14,14 @@ display: flex; align-items: baseline; } +#rule-rules .name > .situationValue { + border-bottom: 2px solid white; + padding-left: 0.4em; +} + +#rule-rules.showValues .situationValue { + display: inline-block; +} .node.inlineExpression:not(.comparison) { padding-left: 0; @@ -33,6 +42,10 @@ text-align: right; } +#rule-rules section { + margin: 1em 0; +} + #declenchement > ul { padding: 0; list-style-type: none; @@ -54,7 +67,7 @@ padding: 0.2em 0.8em; display: inline-block; color: white !important; - font-weight: 600; + font-weight: 500; border-bottom-right-radius: 6px; } .inlineMecanism .name { @@ -109,7 +122,7 @@ } .percentage .name { - font-weight: 600; + font-weight: 500; } .inlineExpression .operator { @@ -129,15 +142,23 @@ border: 1px solid; max-width: 100%; border-radius: 3px; - padding: 1rem; + padding: 1em; + padding-top: 1em; position: relative; flex: 1; } +#rule-rules.showValues .mecanism { + padding-bottom: 2em; +} .mecanism-result { position: absolute; + display: none; bottom: 0px; right: 0; } +#rule-rules.showValues .mecanism-result { + display: initial; +} .mecanism .mecanism { flex: initial; } diff --git a/source/components/Documentation/Algorithm.tsx b/source/components/rule/Algorithm.tsx similarity index 64% rename from source/components/Documentation/Algorithm.tsx rename to source/components/rule/Algorithm.tsx index b5cf89a41..8f5601552 100644 --- a/source/components/Documentation/Algorithm.tsx +++ b/source/components/rule/Algorithm.tsx @@ -1,8 +1,11 @@ +import classNames from 'classnames' import { makeJsx } from 'Engine/evaluation' import { any, identity, path } from 'ramda' import React from 'react' import { Trans } from 'react-i18next' import './Algorithm.css' +// The showValues prop is passed as a context. It used to be delt in CSS (not(.showValues) display: none), both coexist right now +import { ShowValuesProvider } from './ShowValuesContext' let Conditions = ({ 'rendu non applicable': disabledBy, @@ -51,32 +54,33 @@ function ShowIfDisabled({ dependency }) { ) } -export default function Algorithm({ rule }) { +export default function Algorithm({ rule, showValues }) { let formula = - rule.formule || + rule['formule'] || (rule.category === 'variable' && rule.explanation.formule), displayFormula = formula && !!Object.keys(formula).length && !path(['formule', 'explanation', 'une possibilité'], rule) && - !(formula.explanation.constant && rule.nodeValue) + formula.explanation?.category !== 'number' return ( - <> - - {displayFormula && ( - <> -

    Comment cette donnée est-elle calculée ?

    -
    - {makeJsx(formula)} -
    - - )} - +
    +
    + + + {displayFormula && ( +
    +

    + Calcul +

    +
    + {makeJsx(formula)} +
    +
    + )} +
    + {makeJsx(rule['rendu non applicable'])} +
    +
    ) } diff --git a/source/components/Documentation/Destinataire.css b/source/components/rule/Destinataire.css similarity index 100% rename from source/components/Documentation/Destinataire.css rename to source/components/rule/Destinataire.css diff --git a/source/components/Documentation/Destinataire.tsx b/source/components/rule/Destinataire.tsx similarity index 100% rename from source/components/Documentation/Destinataire.tsx rename to source/components/rule/Destinataire.tsx diff --git a/source/components/rule/Examples.js b/source/components/rule/Examples.js new file mode 100644 index 000000000..805e8344d --- /dev/null +++ b/source/components/rule/Examples.js @@ -0,0 +1,79 @@ +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/Documentation/Header.css b/source/components/rule/Header.css similarity index 100% rename from source/components/Documentation/Header.css rename to source/components/rule/Header.css diff --git a/source/components/Documentation/Header.js b/source/components/rule/Header.js similarity index 100% rename from source/components/Documentation/Header.js rename to source/components/rule/Header.js diff --git a/source/components/Documentation/Namespace.css b/source/components/rule/Namespace.css similarity index 100% rename from source/components/Documentation/Namespace.css rename to source/components/rule/Namespace.css diff --git a/source/components/Documentation/Namespace.tsx b/source/components/rule/Namespace.tsx similarity index 86% rename from source/components/Documentation/Namespace.tsx rename to source/components/rule/Namespace.tsx index 34dac6750..ff5378342 100644 --- a/source/components/Documentation/Namespace.tsx +++ b/source/components/rule/Namespace.tsx @@ -1,19 +1,18 @@ import { ThemeColorsContext } from 'Components/utils/colors' -import { SitePathsContext } from 'Components/utils/SitePathsContext' +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 { 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 rules = useContext(EngineContext).getParsedRules() - + const flatRules = useSelector(parsedRulesSelector) return (
      {dottedName @@ -28,7 +27,7 @@ export default function Namespace({ dottedName }: { dottedName: DottedName }) { ) .map((fragments: string[]) => { let ruleName = fragments.join(' . ') as DottedName, - rule = rules[ruleName] + rule = flatRules[ruleName] if (!rule) { throw new Error( `Attention, il se peut que la règle ${ruleName}, ait été définie avec un namespace qui n'existe pas.` diff --git a/source/components/Documentation/References.css b/source/components/rule/References.css similarity index 72% rename from source/components/Documentation/References.css rename to source/components/rule/References.css index 511940ab2..861291afb 100644 --- a/source/components/Documentation/References.css +++ b/source/components/rule/References.css @@ -1,9 +1,12 @@ .references { + font-size: 100%; list-style: none; - padding: 0; + padding-left: 1em; + color: #333350; } .references a { - flex: 1; + color: inherit; + width: 40%; text-decoration: underline; } @@ -17,6 +20,15 @@ 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/Documentation/References.tsx b/source/components/rule/References.tsx similarity index 83% rename from source/components/Documentation/References.tsx rename to source/components/rule/References.tsx index 71141fb55..d8f18a8e6 100644 --- a/source/components/Documentation/References.tsx +++ b/source/components/rule/References.tsx @@ -23,28 +23,16 @@ function Ref({ name, link }: RefProps) { refData = (refKey && references[refKey]) || {}, domain = cleanDomain(link) return ( -
    • +
    • {refData.image && ( )} - + {capitalise0(name)} - {domain} + {domain}
    • ) } diff --git a/source/components/rule/Rule.css b/source/components/rule/Rule.css new file mode 100644 index 000000000..2bfa6c063 --- /dev/null +++ b/source/components/rule/Rule.css @@ -0,0 +1,31 @@ +.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 new file mode 100644 index 000000000..cb70489e2 --- /dev/null +++ b/source/components/rule/Rule.js @@ -0,0 +1,239 @@ +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/Documentation/RuleSource.tsx b/source/components/rule/RuleSource.tsx similarity index 54% rename from source/components/Documentation/RuleSource.tsx rename to source/components/rule/RuleSource.tsx index 70d36d17c..a038f3b81 100644 --- a/source/components/Documentation/RuleSource.tsx +++ b/source/components/rule/RuleSource.tsx @@ -1,6 +1,7 @@ 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' @@ -10,15 +11,13 @@ export default function RuleSource({ dottedName }: RuleSourceProps) { let source = rules[dottedName] return ( -
    -

    Source publicode

    +
    +

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

    -

    - 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/components/rule/ShowValuesContext.tsx b/source/components/rule/ShowValuesContext.tsx new file mode 100644 index 000000000..1fc55612a --- /dev/null +++ b/source/components/rule/ShowValuesContext.tsx @@ -0,0 +1,9 @@ +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 dc05be6ce..c9d5494df 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 ade5295f8..c7ea9640b 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 d722fb76e..3ed92ae1f 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 a580649fb..d272d6c3c 100644 --- a/source/components/simulationConfigs/chômage-partiel.yaml +++ b/source/components/simulationConfigs/chômage-partiel.yaml @@ -1,11 +1,12 @@ objectifs: - contrat salarié . rémunération . brut de base -objectifs cachés: - - contrat salarié . rémunération . net +objectifs secondaires: - contrat salarié . prix du travail + - contrat salarié . rémunération . net - 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 31a35d6dc..a167b3549 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 e6959faca..512437a8b 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: - - contrat salarié . rémunération . net - - dirigeant . indépendant . revenu net de cotisations - - dirigeant . auto-entrepreneur . net de cotisations + - revenu net après impôt + - revenus net de cotisations - protection sociale . retraite - - protection sociale . retraite . trimestres validés + - protection sociale . retraite . trimestres validés par an - protection sociale . santé . indemnités journalières + - protection sociale . accidents du travail et maladies professionnelles questions: uniquement: @@ -19,6 +19,14 @@ questions: - entreprise . catégorie d'activité . libérale règlementée unité par défaut: €/an -situation: - dirigeant: "'auto-entrepreneur'" - contrat salarié . ATMP . taux réduit: oui +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' diff --git a/source/components/simulationConfigs/salarié.yaml b/source/components/simulationConfigs/salarié.yaml index 0850d66d2..0552d5bb5 100644 --- a/source/components/simulationConfigs/salarié.yaml +++ b/source/components/simulationConfigs/salarié.yaml @@ -1,10 +1,16 @@ 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 b8edd01a1..59c1b431d 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?.targetUnit + (state: RootState) => state?.simulation?.defaultUnit ) const previousUnit = useRef(currentUnit) diff --git a/source/components/ui/Toggle.css b/source/components/ui/Toggle.css index 521899321..8a3687eb0 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: 600; + font-weight: 500; } .ui__.toggle input[type='radio']:checked ~ * { diff --git a/source/components/ui/Typography.css b/source/components/ui/Typography.css index fe498dad8..5c562c0f5 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: 600; + font-weight: 500; } textarea { diff --git a/source/components/ui/index.css b/source/components/ui/index.css index 16e1486a5..99ff5340c 100644 --- a/source/components/ui/index.css +++ b/source/components/ui/index.css @@ -126,7 +126,6 @@ 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; @@ -134,10 +133,6 @@ 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 deleted file mode 100644 index f08bf7abd..000000000 --- a/source/components/utils/EngineContext.tsx +++ /dev/null @@ -1,52 +0,0 @@ -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/useNextQuestion.tsx b/source/components/utils/useNextQuestion.tsx deleted file mode 100644 index 1250a181c..000000000 --- a/source/components/utils/useNextQuestion.tsx +++ /dev/null @@ -1,132 +0,0 @@ -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/components/utils/SitePathsContext.tsx b/source/components/utils/withSitePaths.tsx similarity index 100% rename from source/components/utils/SitePathsContext.tsx rename to source/components/utils/withSitePaths.tsx diff --git a/source/engine/RuleInput.tsx b/source/engine/RuleInput.tsx index a0e34d68b..0afdcb65a 100644 --- a/source/engine/RuleInput.tsx +++ b/source/engine/RuleInput.tsx @@ -1,14 +1,13 @@ import Input from 'Components/conversation/Input' import Question from 'Components/conversation/Question' -import SelectGéo from 'Components/conversation/select/SelectGeo' +import SelectGéo from 'Components/conversation/select/SelectGéo' 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, { useContext } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' import { DottedName } from 'Rules' import DateInput from '../components/conversation/DateInput' @@ -18,7 +17,7 @@ export const binaryOptionChoices = [ { value: 'oui', label: 'Oui' } ] -type Value = string | number | object | boolean | null +type Value = string | number | object | boolean export type RuleInputProps = { rules: ParsedRules dottedName: DottedName @@ -47,9 +46,8 @@ export default function RuleInput({ onSubmit }: RuleInputProps) { let rule = rules[dottedName] - let unit = rule.unit + let unit = rule.unit || rule.defaultUnit let language = useTranslation().i18n.language - let engine = useContext(EngineContext) let commonProps = { key: dottedName, @@ -102,11 +100,6 @@ 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 ( <> @@ -130,18 +123,20 @@ export default function RuleInput({ return } -let getVariant = rule => rule?.formule?.explanation['possibilités'] +let getVariant = rule => rule?.formule?.explanation['une possibilité'] 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) - const variants = variant && node.formule.explanation['possibilités'] - const canGiveUp = variant && !node.formule.explanation['choix obligatoire'] + 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'] + return Object.assign( node, - !!variant + shouldBeExpanded ? { canGiveUp, children: variants.map(v => rec(path + ' . ' + v)) diff --git a/source/engine/error.ts b/source/engine/error.ts index eff4b1449..0f16855dc 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 45a4b8d3e..7630697cc 100644 --- a/source/engine/evaluateRule.ts +++ b/source/engine/evaluateRule.ts @@ -3,6 +3,7 @@ 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, @@ -47,11 +48,8 @@ export const evaluateApplicability = ( ]) return { - ...node, - isApplicable, nodeValue: isApplicable, missingVariables, - parentDependencies, ...evaluatedAttributes } } @@ -74,7 +72,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 - let evaluatedFormula = isApplicable + const evaluatedFormula = isApplicable ? evaluateFormula() : isApplicable === false ? { @@ -87,10 +85,27 @@ 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 - if (node.unit) { + const temporalValue = evaluatedFormula.temporalValue + if (unit) { try { - evaluatedFormula = convertNodeToUnit(node.unit, evaluatedFormula) + nodeValue = convertNodeToUnit(unit, evaluatedFormula).nodeValue } catch (e) { typeWarning( node.dottedName, @@ -99,20 +114,14 @@ 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: evaluatedFormula.nodeValue, - unit: node.unit ?? evaluatedFormula.unit, + nodeValue, + unit, temporalValue, isApplicable, missingVariables diff --git a/source/engine/evaluation.tsx b/source/engine/evaluation.tsx index 5fdff9a95..7e643d97f 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,11 +21,10 @@ 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.unit) + ? node.jsx(node.nodeValue, node.explanation, node.lazyEval, node.unit) : node.jsx export let collectNodeMissing = node => node.missingVariables || {} @@ -41,6 +40,12 @@ 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 } @@ -170,7 +175,6 @@ export let evaluateObject = (objectShape, effect) => ( }, temporalExplanations) const sameUnitTemporalExplanation: Temporal> = convertNodesToSameUnit( temporalExplanation.map(x => x.value), @@ -206,22 +210,3 @@ 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 c29e1beed..31a091de5 100644 --- a/source/engine/format.test.js +++ b/source/engine/format.test.js @@ -1,62 +1,48 @@ import { expect } from 'chai' import { parseUnit } from 'Engine/units' -import { formatValue } from './format' +import { formatCurrency, formatPercentage, formatValue } from './format' describe('format engine values', () => { it('format currencies', () => { - 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') + 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') }) it('format percentages', () => { - 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%') + expect(formatPercentage(10)).to.equal('10%') + expect(formatPercentage(100)).to.equal('100%') + expect(formatPercentage(10.2)).to.equal('10.2%') }) it('format values', () => { - 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( + 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( '12 €' ) - expect(formatValue({ nodeValue: 1200, language: 'fr' })).to.equal('1 200') + expect(formatValue({ value: 1200, language: 'fr' })).to.equal('1 200') }) }) describe('Units handling', () => { it('translate unit', () => { - expect( - formatValue({ nodeValue: 1, unit: 'jour', language: 'fr' }) - ).to.equal('1 jour') - expect( - formatValue({ nodeValue: 1, unit: 'jour', language: 'en' }) - ).to.equal('1 day') + expect(formatValue({ value: 1, unit: 'jour', language: 'fr' })).to.equal( + '1 jour' + ) + expect(formatValue({ value: 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({ nodeValue: 2, unit: 'jour', language: 'fr' }) - ).to.equal('2 jours') - expect( - formatValue({ - nodeValue: 7, - unit: parseUnit('jour/semaine'), - language: 'fr' - }) + formatValue({ value: 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 684156b31..d0049718a 100644 --- a/source/engine/format.ts +++ b/source/engine/format.ts @@ -1,6 +1,5 @@ import { serializeUnit } from 'Engine/units' import { memoizeWith } from 'ramda' -import { Evaluation } from './types' import { Unit } from './units' const NumberFormat = memoizeWith( @@ -47,7 +46,7 @@ export const currencyFormat = (language: string | undefined) => ({ export const formatCurrency = (value: number | undefined, language: string) => { return value == null ? '' - : (formatNumber({ unit: '€', language, value }) ?? '').replace( + : (formatValue({ unit: '€', language, value }) ?? '').replace( /^(-)?€/, '$1€\u00A0' ) @@ -56,17 +55,17 @@ export const formatCurrency = (value: number | undefined, language: string) => { export const formatPercentage = (value: number | undefined) => value == null ? '' - : formatNumber({ unit: '%', value, maximumFractionDigits: 2 }) + : formatValue({ unit: '%', value, maximumFractionDigits: 2 }) export type formatValueOptions = { maximumFractionDigits?: number minimumFractionDigits?: number language?: string unit?: Unit | string - value: number + value?: number } -function formatNumber({ +export function formatValue({ maximumFractionDigits, minimumFractionDigits, language, @@ -107,44 +106,3 @@ function formatNumber({ ) } } - -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 new file mode 100644 index 000000000..f7b9ea11b --- /dev/null +++ b/source/engine/generateQuestions.ts @@ -0,0 +1,60 @@ +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 19b72a66f..8faf4b803 100644 --- a/source/engine/getSituationValue.js +++ b/source/engine/getSituationValue.js @@ -21,11 +21,21 @@ 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 d2883befc..e80a5ead4 100644 --- a/source/engine/index.ts +++ b/source/engine/index.ts @@ -1,21 +1,24 @@ import { evaluateControls } from 'Engine/controls' -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 { ParsedRules, Rules } from 'Engine/types' import { Simulation } from 'Reducers/rootReducer' -import { evaluationError, warning } from './error' -import { collectDefaults, evaluateNode } from './evaluation' +import { evaluateNode } from './evaluation' import parseRules from './parseRules' +import { collectDefaults } from './ruleUtils' +import { parseUnit, Unit } from './units' -const emptyCache = () => ({ - _meta: { contextRule: [] } -}) +const emptyCache = { + _meta: { contextRule: [], defaultUnits: [] } +} + +type EngineConfig = { + rules: string | Rules | ParsedRules + useDefaultValues?: boolean +} type Cache = { _meta: { contextRule: Array + defaultUnits: Array inversionFail?: { given: string estimated: string @@ -23,135 +26,70 @@ 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 - cacheWithoutDefault: Cache + cache: Cache = { ...emptyCache } - constructor(rules: string | Rules | ParsedRules) { - this.cache = emptyCache() - this.cacheWithoutDefault = emptyCache() + constructor({ rules, useDefaultValues = true }: EngineConfig) { this.parsedRules = typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName ? parseRules(rules) : (rules as ParsedRules) - - this.defaultValues = mapObjIndexed( - (value, name) => - typeof value === 'string' - ? this.evaluateExpression(value, `[valeur par défaut] ${name}`, false) - : value, - collectDefaults(this.parsedRules) - ) + this.defaultValues = useDefaultValues + ? collectDefaults(this.parsedRules) + : {} } private resetCache() { - this.cache = emptyCache() - this.cacheWithoutDefault = emptyCache() + this.cache = { ...emptyCache } } - private situationGate(useDefaultValues = true) { - return dottedName => - this.situation[dottedName] ?? - (useDefaultValues ? this.defaultValues[dottedName] : 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) - ) - ) - - 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> = {} - ) { + setSituation(situation: Simulation['situation'] = {}) { + this.situation = situation 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 + 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) ) - 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 + return Array.isArray(expression) ? results : results[0] } 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 631c7e6e6..2e02e1611 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 1df8d11ca..a24711323 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: 600; + font-weight: 500; } .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 d21dd4d06..25eb33fa3 100644 --- a/source/engine/mecanismViews/Barème.tsx +++ b/source/engine/mecanismViews/Barème.tsx @@ -4,9 +4,8 @@ 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 (
      @@ -23,7 +22,7 @@ export default function Barème(nodeValue, explanation, unit) { )} diff --git a/source/engine/mecanismViews/Composantes.js b/source/engine/mecanismViews/Composantes.js index 22eae557c..0eb8678de 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 d5192a7c2..8fa8f9038 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 aaf3df448..797752c43 100644 --- a/source/engine/mecanismViews/InversionNumérique.js +++ b/source/engine/mecanismViews/InversionNumérique.js @@ -1,53 +1,49 @@ +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 ( - - {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)}
        • - ))} -
        - + + {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 : +

        + + + )} +
        )} -
        + ) } diff --git a/source/engine/mecanismViews/Product.js b/source/engine/mecanismViews/Product.js index 6a1e13f70..e9c5f81a1 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.recalcul && ( + {explanation.règle && ( - Recalcul de la règle{' '} - avec les - valeurs suivantes : + Calcul de avec : )}
          diff --git a/source/engine/mecanismViews/TauxProgressif.tsx b/source/engine/mecanismViews/TauxProgressif.tsx index 4259c8dda..f9571d272 100644 --- a/source/engine/mecanismViews/TauxProgressif.tsx +++ b/source/engine/mecanismViews/TauxProgressif.tsx @@ -3,9 +3,8 @@ 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: Unit) { +export default function TauxProgressif(nodeValue, explanation, _, unit) { return ( Taux calculé :{' '} {' '} - + )}
        diff --git a/source/engine/mecanismViews/Variations.js b/source/engine/mecanismViews/Variations.js index 116e01f3f..fac9ab951 100644 --- a/source/engine/mecanismViews/Variations.js +++ b/source/engine/mecanismViews/Variations.js @@ -1,4 +1,5 @@ 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' @@ -11,103 +12,109 @@ let Comp = function Variations({ nodeValue, explanation, unit }) { let [expandedVariation, toggleVariation] = useState(null) const { i18n } = useTranslation() return ( - - {' '} - <> -

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

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

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

          +
            + {explanation.map(({ condition, consequence, satisfied }, i) => ( +
          1. + {!satisfied && showValues && ( + <> + non applicable + {expandedVariation !== i ? ( + + ) : ( + + )} + )} - - )} - {(expandedVariation === i || satisfied || nodeValue == null) && ( -
            - {!condition.isDefault && ( -
            - Si : {makeJsx(condition)} + {(expandedVariation === i || satisfied || !showValues) && ( +
            + {!condition.isDefault && ( +
            + Si : {makeJsx(condition)} +
            + )} +
            + + {!condition.isDefault ? ( + Alors + ) : ( + Sinon + )}{' '} + :  + + + {consequence && makeJsx(consequence)} + +
            )} -
            - - {!condition.isDefault ? ( - Alors - ) : ( - Sinon - )}{' '} - :  - - - {consequence && makeJsx(consequence)} - -
            -
            - )} -
          2. - ))} -
          - -
          +
        2. + ))} +
        + +
        + )} + ) } // 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 1e5598ad7..397b4699f 100644 --- a/source/engine/mecanismViews/common.tsx +++ b/source/engine/mecanismViews/common.tsx @@ -1,24 +1,22 @@ import { default as classNames, default as classnames } from 'classnames' -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 { SitePathsContext } from 'Components/utils/withSitePaths' +import Value, { ValueProps } from 'Components/Value' +import { ParsedRule } 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: Evaluation - unit: Unit + data: ValueProps['nodeValue'] + unit: ValueProps['unit'] } export let NodeValuePointer = ({ data, unit }: NodeValuePointerProps) => ( @@ -31,12 +29,13 @@ 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' })} + ) @@ -85,7 +84,14 @@ export function Node({
    ) ) : ( - + {(value as any) !== true && (value as any) !== false && !isNil(value) && } @@ -111,45 +117,52 @@ export function InlineMecanism({ name }: { name: string }) { } type LeafProps = { - className: string - rule: ParsedRule + classes: string + dottedName: DottedName + name: string 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({ className, rule, nodeValue, filter, unit }: LeafProps) { +export function Leaf({ + classes, + dottedName, + name, + nodeValue, + filter, + unit +}: LeafProps) { const sitePaths = useContext(SitePathsContext) - const useDefaultValues = useContext(UseDefaultValuesContext) - const title = rule.title || capitalise0(rule.name) + const rules = useSelector(parsedRulesSelector) + let rule = rules[dottedName] + const title = rule.title || capitalise0(name) return ( - - - - - {rule.acronyme ? {rule.acronyme} : title}{' '} - {filter} - - - {!isNil(nodeValue) && ( - - - - )} - + + {dottedName && ( + + + + {rule.acronyme ? ( + {rule.acronyme} + ) : ( + title + )}{' '} + {filter} + + + {!isNil(nodeValue) && ( + + + + )} + + )} ) } diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index 27f07ae73..ad2563cef 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -1,15 +1,11 @@ import { decompose } from 'Engine/mecanisms/utils' import variations from 'Engine/mecanisms/variations' import { convertNodeToUnit } from 'Engine/nodeUnits' -import { - areUnitConvertible, - convertUnit, - inferUnit, - serializeUnit -} from 'Engine/units' +import { inferUnit, isPercentUnit } from 'Engine/units' import { any, equals, + evolve, is, map, max, @@ -17,7 +13,8 @@ import { min, path, pluck, - reduce + reduce, + toPairs } from 'ramda' import React from 'react' import { typeWarning } from './error' @@ -37,6 +34,7 @@ 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' @@ -134,97 +132,135 @@ export let mecanismAllOf = (recurse, k, v) => { } } -let evaluateInversion = (oldCache, situationGate, parsedRules, node) => { - // TODO : take applicability into account here - let inversedWith = node.explanation.inversionCandidates.find( - n => situationGate(n.dottedName) != undefined +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) ) - if (!inversedWith) { +} + +let doInversion = (oldCache, situationGate, parsedRules, v, dottedName) => { + let inversion = findInversion(situationGate, parsedRules, v, dottedName) + + if (!inversion) return { - ...node, - missingVariables: { - ...Object.fromEntries( - node.explanation.inversionCandidates.map(n => [n.dottedName, 1]) - ), - [node.explanation.ruleToInverse]: 1 - }, + missingVariables: { [dottedName]: 1 }, nodeValue: null } - } - inversedWith = evaluateNode( - oldCache, - situationGate, - parsedRules, - inversedWith - ) + let { fixedObjectiveValue, fixedObjectiveRule } = inversion - const evaluateWithValue = n => - evaluateNode( - { - _meta: oldCache._meta - }, - dottedName => - dottedName === node.explanation.ruleToInverse - ? n - : dottedName === inversedWith.dottedName + 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(dottedName), + : situationGate(n), parsedRules, - inversedWith + fixedObjectiveRule ) + return v + } // 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 - 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 attempt = fx(2000) + if (attempt.nodeValue == null) { + return attempt } + 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 { - ...node, - nodeValue: nodeValue ?? null, - explanation: { - ...node.explanation, - inversionFail: nodeValue === undefined, - inversedWith - }, - missingVariables: randomAttempt.missingVariables + nodeValue, + missingVariables: {}, + inversionCache, + inversedWith: { + rule: fixedObjectiveRule, + value: fixedObjectiveValue + } } } export let mecanismInversion = dottedName => (recurse, k, v) => { - if (!v.avec) { - throw new Error( - "Une formule d'inversion doit préciser _avec_ quoi on peut inverser la variable" - ) + 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 } + return { - evaluate: evaluateInversion, + ...v, + evaluate, unit: v.unité && parseUnit(v.unité), - explanation: { - ruleToInverse: dottedName, - inversionCandidates: v.avec.map(recurse), - negativeValuesAllowed: v['valeurs négatives possibles'] === 'oui' - }, + explanation: evolve({ avec: map(recurse) }, v), jsx: InversionNumérique, category: 'mecanism', name: 'inversion numérique', @@ -233,57 +269,61 @@ export let mecanismInversion = dottedName => (recurse, k, v) => { } export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => { - let evaluate = (cache, situationGate, parsedRules, node) => { - if (cache._meta.inRecalcul) { + 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) { 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] - } - const evaluatedNode = evaluateNode( - recalculCache, + let amendedSituationGate = dottedName => + Object.keys(amendedSituation).includes(dottedName) + ? evaluateNode( + cache, + amendedSituationGate, + parsedRules, + recurse(amendedSituation[dottedName]) + ).nodeValue + : situationGate(dottedName) + + let evaluatedNode = evaluateNode( + cache, amendedSituationGate, parsedRules, - node.explanation.recalcul + nodeToEvaluate ) return { - ...node, - nodeValue: evaluatedNode.nodeValue, - ...(evaluatedNode.temporalValue && { - temporalValue: evaluatedNode.temporalValue - }), - unit: evaluatedNode.unit, + ...evaluatedNode, explanation: { - recalcul: evaluatedNode, - amendedSituation - } + ...evaluateNode.explanation, + unit: evaluatedNode.unit, + amendedSituation: Object.fromEntries( + Object.keys(amendedSituation).map(dottedName => [ + dottedName, + evaluateNode( + cache, + amendedSituationGate, + parsedRules, + recurse(amendedSituation[dottedName]) + ) + ]) + ) + }, + jsx: Recalcul } } - 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 { - explanation: { - recalcul: nodeToEvaluate, - amendedSituation - }, - jsx: Recalcul, + ...v, evaluate } } @@ -299,7 +339,7 @@ export let mecanismSum = (recurse, k, v) => { return { evaluate, // eslint-disable-next-line - jsx: (nodeValue, explanation, unit) => ( + jsx: (nodeValue, explanation, _, unit) => ( ), explanation, @@ -331,7 +371,7 @@ export let mecanismReduction = (recurse, k, v) => { try { franchise = convertNodeToUnit(assiette.unit, franchise) plafond = convertNodeToUnit(assiette.unit, plafond) - if (serializeUnit(abattement.unit) !== '%') { + if (!isPercentUnit(abattement.unit)) { abattement = convertNodeToUnit(assiette.unit, abattement) } if (décote) { @@ -364,13 +404,13 @@ export let mecanismReduction = (recurse, k, v) => { ? montantFranchiséDécoté === 0 ? 0 : null - : serializeUnit(abattement.unit) === '%' + : isPercentUnit(abattement.unit) ? max( 0, montantFranchiséDécoté - min( plafond.nodeValue, - (abattement.nodeValue / 100) * montantFranchiséDécoté + abattement.nodeValue * montantFranchiséDécoté ) ) : max( @@ -437,10 +477,10 @@ export let mecanismProduct = (recurse, k, v) => { ) } } - const mult = (base, rate, facteur, plafond) => + let mult = (base, rate, facteur, plafond) => Math.min(base, plafond === false ? Infinity : plafond) * rate * facteur - let nodeValue = [taux, assiette, facteur].some(n => n.nodeValue === false) + const nodeValue = [taux, assiette, facteur].some(n => n.nodeValue === false) ? false : [taux, assiette, facteur].some(n => n.nodeValue === 0) ? 0 @@ -453,14 +493,10 @@ export let mecanismProduct = (recurse, k, v) => { plafond.nodeValue ) - let unit = inferUnit( + const 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, @@ -493,18 +529,6 @@ 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 c06533ad0..8e0aec02b 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 24df2d4c7..f7eb52be3 100644 --- a/source/engine/mecanisms/arrondi.tsx +++ b/source/engine/mecanisms/arrondi.tsx @@ -5,22 +5,21 @@ 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: Evaluation + nodeValue: EvaluatedRule['nodeValue'] explanation: ArrondiExplanation } type ArrondiExplanation = { - value: EvaluatedNode - decimals: EvaluatedNode + value: EvaluatedRule + decimals: EvaluatedRule } function MecanismRound({ nodeValue, explanation }: MecanismRoundProps) { @@ -33,17 +32,16 @@ function MecanismRound({ nodeValue, explanation }: MecanismRoundProps) { > <> {makeJsx(explanation.value)} - {explanation.decimals.nodeValue !== false && - explanation.decimals.isDefault != false && ( -

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

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

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

    + )} ) @@ -65,7 +63,7 @@ function evaluate( situation, parsedRules ) - const value = simplifyNodeUnit(evaluateAttribute(node.explanation.value)) + const value = 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 20f29526b..d7de95d5f 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( @@ -53,17 +54,23 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => { let temporalValue = liftTemporal2( (a, b) => { - if (!['≠', '='].includes(node.operator) && a === false && b === false) { + if (['∕', '-'].includes(node.operator) && a === 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 ( - ['≠', '=', '<', '>', '≤', '≥'].includes(node.operator) && + comparisonOperator.includes(node.operator) && [a, b].every(value => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/)) ) { return operatorFunction(convertToDate(a), convertToDate(b)) @@ -73,6 +80,7 @@ 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 { @@ -86,7 +94,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 d3e761d2f..5ed679cc6 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 23cdf3fc7..a0032d3af 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/types' +import { Evaluation } from 'Engine/temporal' 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 b8ddb818a..213de083d 100644 --- a/source/engine/mecanisms/variations.ts +++ b/source/engine/mecanisms/variations.ts @@ -11,6 +11,7 @@ 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 */ @@ -146,7 +147,12 @@ function evaluate( liftTemporal2(or, previousConditions, currentCondition) ] }, - [pureTemporal(false), [], node.unit, pureTemporal(false)] + [ + pureTemporal(false), + [], + getNodeDefaultUnit({ defaultUnit: node.unit }, cache), + pureTemporal(false) + ] ) const nodeValue = temporalAverage(temporalValue, unit) diff --git a/source/engine/nodeUnits.ts b/source/engine/nodeUnits.ts index a705086c0..b41e669e7 100644 --- a/source/engine/nodeUnits.ts +++ b/source/engine/nodeUnits.ts @@ -1,33 +1,55 @@ -import { mapTemporal } from './temporal' -import { convertUnit, simplifyUnit, Unit } from './units' -import { EvaluatedNode } from './types' +import { EvaluatedNode, mapTemporal } from './temporal' +import { + areUnitConvertible, + convertUnit, + simplifyUnitWithValue, + Unit +} from './units' export function simplifyNodeUnit(node) { - if (!node.unit) { + if (!node.unit || node.nodeValue === false || node.nodeValue == null) { return node } - const unit = simplifyUnit(node.unit) + const [unit, nodeValue] = simplifyUnitWithValue(node.unit, node.nodeValue) - return convertNodeToUnit(unit, node) + return { + ...node, + unit, + nodeValue + } +} +export const getNodeDefaultUnit = (node, cache) => { + if ( + node.question && + node.unit == null && + node.defaultUnit == null && + node.formule?.unit == null + ) { + return false + } + + return ( + node.unit || + cache._meta.defaultUnits.find(unit => + areUnitConvertible(node.defaultUnit, unit) + ) || + node.defaultUnit + ) } -export function convertNodeToUnit( - to: Unit, - node: EvaluatedNode -) { - const temporalValue = - node.temporalValue && node.unit - ? mapTemporal( - value => convertUnit(node.unit, to, value), - node.temporalValue - ) - : node.temporalValue +export function convertNodeToUnit(to: Unit, node: EvaluatedNode) { return { ...node, nodeValue: node.unit ? convertUnit(node.unit, to, node.nodeValue) : node.nodeValue, - ...(temporalValue && { temporalValue }), + temporalValue: + node.temporalValue && node.unit + ? mapTemporal( + value => convertUnit(node.unit, to, value), + node.temporalValue + ) + : node.temporalValue, unit: to } } diff --git a/source/engine/parse.tsx b/source/engine/parse.tsx index a8604afde..a81d45bb8 100644 --- a/source/engine/parse.tsx +++ b/source/engine/parse.tsx @@ -2,6 +2,7 @@ // 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' @@ -28,7 +29,6 @@ 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) -const parseExpression = (rule, rawNode) => { +export 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 1a0314d9f..fa0f0e8e0 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 } from './nodeUnits' +import { convertNodeToUnit, getNodeDefaultUnit } from './nodeUnits' import { disambiguateRuleReference } from './ruleUtils' -import { areUnitConvertible, serializeUnit } from './units' +import { areUnitConvertible } from './units' const getApplicableReplacements = ( filter, contextRuleName, @@ -56,7 +56,7 @@ const getApplicableReplacements = ( if (referenceNode.question && situationValue == null) { missingVariableList.push({ [referenceNode.dottedName]: 1 }) } - return situationValue?.nodeValue !== false + return situationValue !== 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 = rule.unit + const replacedRuleUnit = getNodeDefaultUnit(rule, cache) if (!areUnitConvertible(replacementNode.unit, replacedRuleUnit)) { typeWarning( contextRuleName, @@ -149,51 +149,34 @@ 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) => { + let cacheNode = (nodeValue, missingVariables, explanation, temporalValue) => { cache[cacheName] = { ...node, nodeValue, + temporalValue, ...(explanation && { explanation }), - ...(explanation?.temporalValue && { - temporalValue: explanation.temporalValue - }), ...(explanation?.unit && { unit: explanation.unit }), missingVariables } return addReplacementMissingVariable(cache[cacheName]) } - const applicabilityEvaluation = evaluateApplicability( - cache, - situation, - rules, - rule - ) - if (!applicabilityEvaluation.nodeValue) { - return cacheNode( - applicabilityEvaluation.nodeValue, - applicabilityEvaluation.missingVariables, - applicabilityEvaluation - ) + const { + nodeValue: isApplicable, + missingVariables: condMissingVariables + } = evaluateApplicability(cache, situation, rules, rule) + if (!isApplicable) { + return cacheNode(isApplicable, condMissingVariables, rule) } const situationValue = getSituationValue(situation, dottedName, rule) - 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 (situationValue !== undefined) { + const unit = getNodeDefaultUnit(rule, cache) + return cacheNode(situationValue, condMissingVariables, { + ...rule, + nodeValue: situationValue, + unit + }) } if (rule.formule != null) { @@ -227,17 +210,20 @@ 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 + const unit = + parsedRule.unit || parsedRule.formule?.unit || parsedRule.defaultUnit return { evaluate: evaluateReference(filter, rule.dottedName), //eslint-disable-next-line react/display-name - jsx: (nodeValue, _, nodeUnit) => ( + jsx: (nodeValue, explanation, _, nodeUnit) => ( <> ), @@ -245,7 +231,6 @@ export let parseReference = ( category: 'reference', partialReference, dottedName, - explanation: parsedRule, unit } } diff --git a/source/engine/parseRule.tsx b/source/engine/parseRule.tsx index 9d1788169..7a1057375 100644 --- a/source/engine/parseRule.tsx +++ b/source/engine/parseRule.tsx @@ -1,3 +1,4 @@ +import { ShowValuesConsumer } from 'Components/rule/ShowValuesContext' import RuleLink from 'Components/RuleLink' import { evolve, map } from 'ramda' import React from 'react' @@ -14,7 +15,7 @@ import { nameLeaf } from './ruleUtils' import { ParsedRule, Rule, Rules } from './types' -import { parseUnit, simplifyUnit } from './units' +import { parseUnit } from './units' export default function( rules: Rules, @@ -48,32 +49,32 @@ export default function( } rawRule as Rule - 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é && 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é`' ) } - 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, - parentDependencies, - defaultValue: rawRule['par défaut'] + defaultUnit, + parentDependencies } let parsedRule = evolve({ @@ -85,14 +86,19 @@ export default function( parents.map(parent => { let node = parse(rules, rule, parsedRules)(parent) - 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 + 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 + } +
    + ) return { evaluate: (cache, situation, parsedRules) => @@ -117,10 +123,6 @@ 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( @@ -183,10 +185,7 @@ export default function( ...parsedRule, evaluate, parsed: true, - unit: - parsedRule.unit ?? - (parsedRule.formule?.unit && simplifyUnit(parsedRule.formule.unit)) ?? - parsedRule.defaultValue?.unit, + defaultUnit: parsedRule.defaultUnit || parsedRule.formule?.unit, isDisabledBy: [], replacedBy: [] } @@ -248,12 +247,12 @@ let evolveCond = (dottedName, rule, rules, parsedRules) => value => { let child = parse(rules, rule, parsedRules)(value) - let jsx = (nodeValue, explanation, unit) => ( + let jsx = (nodeValue, explanation) => ( {explanation.category === 'variable' ? (
    {makeJsx(explanation)}
    diff --git a/source/engine/react.tsx b/source/engine/react.tsx new file mode 100644 index 000000000..db038cad3 --- /dev/null +++ b/source/engine/react.tsx @@ -0,0 +1,53 @@ +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 5d7d19b58..3dc507f44 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, Rule, Rules } from './types' +import { EvaluatedRule, ParsedRule, ParsedRules, Rule, Rules } from './types' export const splitName = str => str.split(' . ') export const joinName = strs => strs.join(' . ') @@ -52,6 +52,22 @@ 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 8fd545ebe..f307198e3 100644 --- a/source/engine/temporal.ts +++ b/source/engine/temporal.ts @@ -6,7 +6,6 @@ import { getRelativeDate, getYear } from 'Engine/date' -import { EvaluatedNode, Evaluation, Types } from './types' import { Unit } from './units' export type Period = { @@ -64,12 +63,22 @@ export function parsePeriod(word: string, date: Date): Period { throw new Error('Non implémenté') } -export type TemporalNode = Temporal< - EvaluatedNode -> +// 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 Temporal = Array & { value: T }> -export function narrowTemporalValue( +export function narrowTemporalValue( period: Period, temporalValue: Temporal> ): Temporal> { @@ -81,7 +90,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> { @@ -144,16 +153,10 @@ export function concatTemporals( ) } -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> +export function liftTemporalNode(node: EvaluatedNode): TemporalNode { + const { temporalValue, ...baseNode } = node + if (!temporalValue) { + return pureTemporal(baseNode) } return mapTemporal( nodeValue => ({ diff --git a/source/engine/types.ts b/source/engine/types.ts index 288f5c843..19c78b003 100644 --- a/source/engine/types.ts +++ b/source/engine/types.ts @@ -1,4 +1,3 @@ -import { Temporal } from './temporal' import { Unit } from './units' type Contrôle = { @@ -22,7 +21,7 @@ export type Rule = { titre?: string type?: string note?: string - suggestions?: { [description: string]: number } + suggestions?: { [description: string]: string | number } références?: { [source: string]: string } contrôles?: Array } @@ -41,6 +40,7 @@ export type ParsedRule = Rule & { API?: Object icons?: string formule?: any + suggestions?: Object evaluate?: Function explanation?: any isDisabledBy?: Array @@ -54,36 +54,16 @@ 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, - Type extends Types = Types -> = ParsedRule & - EvaluatedNode & { - isApplicable: boolean - explanation: Explanation - } + Explanation = ParsedRule +> = ParsedRule & { + nodeValue?: number + isDefault?: boolean + isApplicable: boolean + missingVariables: Array + explanation: Explanation +} diff --git a/source/engine/units.ts b/source/engine/units.ts index a4567e01d..474b67c38 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 './types' +import { Evaluation } from './temporal' type BaseUnit = string @@ -163,11 +163,13 @@ function singleUnitConversionFactor( ) } function unitsConversionFactor(from: string[], to: string[]): number { - let factor = - 100 ** - // Factor is mutliplied or divided 100 for each '%' in units - (to.filter(unit => unit === '%').length - - from.filter(unit => unit === '%').length) + let factor = 1 + if (to.includes('%')) { + factor *= 100 + } + if (from.includes('%')) { + factor /= 100 + } ;[factor] = from.reduce( ([value, toUnits], fromUnit) => { const index = toUnits.findIndex( @@ -239,19 +241,11 @@ function areSameClass(a: string, b: string) { function round(value: number) { return +value.toFixed(16) } -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] { +export function simplifyUnitWithValue( + unit: Unit, + value: number = 1 +): [Unit, number] { const { denominators, numerators } = unit - const factor = unitsConversionFactor(numerators, denominators) return [ simplify( @@ -274,13 +268,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, @@ -292,3 +286,13 @@ 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 2bb4f3ead..2b9334452 100644 --- a/source/locales/en.yaml +++ b/source/locales/en.yaml @@ -5,7 +5,6 @@ <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 @@ -123,6 +122,7 @@ 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 @@ -152,7 +152,6 @@ 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 @@ -911,6 +910,7 @@ 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 74fee4585..96785efc0 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 the LODEOM eligible sectors?' - question.fr: Votre entreprise appartient-elle à l'un des secteurs éligible LODEOM ? + question.en: '[automatic] Does your company belong to one of these sectors?' + question.fr: Votre entreprise appartient-elle à l'un de ces secteurs ? titre.en: '[automatic] business areas' titre.fr: secteurs d'activité contrat salarié . lodeom . zone un: @@ -2892,6 +2892,9 @@ 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? @@ -3268,7 +3271,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 . heures supplémentaires: +contrat salarié . rémunération . taux horaire des heures supplémentaires et complémentaires: description.en: > [automatic] The hourly rate used to calculate overtime pay. It includes benefits in kind and bonuses for work performed. @@ -3276,8 +3279,8 @@ contrat salarié . rémunération . taux horaire . heures supplémentaires: 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 (overtime)' - titre.fr: taux horaire (heure supplémentaire) + titre.en: '[automatic] hourly rate for overtime and overtime' + titre.fr: taux horaire des heures supplémentaires et complémentaires contrat salarié . rémunération . total: description.en: >- It is the gross salary, plus the employer contributions. It is the total @@ -4370,6 +4373,9 @@ 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 @@ -5881,13 +5887,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: - titre.en: '[automatic] validated quarters' - titre.fr: trimestres validés -protection sociale . retraite . trimestres validés . barème trimestres générique: +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: titre.en: generic quarters scale titre.fr: barème trimestres générique -protection sociale . retraite . trimestres validés . trimestres auto-entrepreneur: +protection sociale . retraite . trimestres validés par an . 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 @@ -5898,10 +5904,10 @@ protection sociale . retraite . trimestres validés . trimestres auto-entreprene 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 . trimestres indépendant: +protection sociale . retraite . trimestres validés par an . trimestres indépendant: titre.en: self-employed quarters titre.fr: trimestres indépendant -protection sociale . retraite . trimestres validés . trimestres salarié: +protection sociale . retraite . trimestres validés par an . 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 984635a08..c6184add1 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, omit, without } from 'ramda' +import { defaultTo, identity, omit, without } from 'ramda' import reduceReducers from 'reduce-reducers' import { combineReducers, Reducer } from 'redux' -import originRules, { Rules } from 'Rules' +import originRules, { DottedName, Rules } from 'Rules' +import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' import { SavedSimulation } from 'Selectors/storageSelectors' import i18n, { AvailableLangs } from '../i18n' -import { DottedName } from './../rules/index' -import { objectifsSelector } from '../selectors/simulationSelectors' +import { areUnitConvertible, convertUnit, parseUnit } from './../engine/units' import inFranceAppReducer, { Company } from './inFranceAppReducer' import storageRootReducer from './storageReducer' @@ -36,6 +36,15 @@ 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': @@ -68,22 +77,82 @@ 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 = { +export type SimulationConfig = Partial<{ objectifs: | Array | Array<{ icône: string; nom: string; objectifs: Array }> + questions: Partial>> + bloquant: Array situation: Simulation['situation'] - bloquant?: Array - questions?: Partial>> - branches?: Array<{ nom: string; situation: SimulationConfig['situation'] }> + branches: Array<{ nom: string; situation: SimulationConfig['situation'] }> 'unité par défaut': string -} +}> type Situation = Partial> export type Simulation = { @@ -92,14 +161,14 @@ export type Simulation = { hiddenControls: Array situation: Situation initialSituation: Situation - targetUnit: string + defaultUnit: string foldedSteps: Array unfoldedStep?: DottedName | null } function getCompanySituation(company: Company): Situation { return { ...(company?.localisation && { - 'établissement . localisation': company.localisation + 'établissement . localisation': JSON.stringify(company.localisation) }), ...(company?.dateDeCréation && { 'entreprise . date de création': company.dateDeCréation.replace( @@ -113,6 +182,7 @@ function getCompanySituation(company: Company): Situation { function simulation( state: Simulation | null = null, action: Action, + analysis: any, existingCompany: Company ): Simulation | null { if (action.type === 'SET_SIMULATION') { @@ -129,7 +199,7 @@ function simulation( hiddenControls: [], situation: companySituation, initialSituation: companySituation, - targetUnit: config['unité par défaut'] || '€/mois', + defaultUnit: config['unité par défaut'] || '€/mois', foldedSteps: Object.keys(companySituation) as Array, unfoldedStep: null } @@ -150,20 +220,13 @@ 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: { - ...(targets.includes(dottedName) - ? omit(targets, situation) - : situation), - [dottedName]: value - } + situation: updateSituation(state.situation, { + fieldName: action.fieldName, + value: action.value, + analysis + }) } case 'STEP_ACTION': const { name, step } = action @@ -181,10 +244,14 @@ function simulation( } } return state - case 'UPDATE_TARGET_UNIT': + case 'UPDATE_DEFAULT_UNIT': return { ...state, - targetUnit: action.targetUnit + situation: updateDefaultUnit(state.situation, { + toUnit: action.defaultUnit, + analysis + }), + defaultUnit: action.defaultUnit } } return state @@ -207,11 +274,18 @@ 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, state?.inFranceApp?.existingCompany), + simulation( + a, + b, + a && analysisWithDefaultsSelector(state), + 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 a35e8d784..2e58cd31b 100644 --- a/source/rules/artiste-auteur.yaml +++ b/source/rules/artiste-auteur.yaml @@ -6,7 +6,8 @@ artiste-auteur . revenus: artiste-auteur . revenus . traitements et salaires: titre: Revenu en traitements et salaires - par défaut: 0 €/an + unité par défaut: €/an + par défaut: 0 résumé: Le montant brut hors TVA de vos droits d'auteur (recettes précomptées) artiste-auteur . revenus . BNC: @@ -30,11 +31,13 @@ artiste-auteur . revenus . BNC . micro-bnc: artiste-auteur . revenus . BNC . recettes: titre: Revenu en BNC - par défaut: 0 €/an + unité par défaut: €/an + par défaut: 0 résumé: Le montant de vos recettes brutes hors TVA artiste-auteur . revenus . BNC . frais réels: - par défaut: 0 €/an + unité par défaut: €/an + par défaut: 0 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 9970977d5..77f359481 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 . repas forfaitaire + règle: rémunération . avantages en nature . nourriture . montant forfaitaire d'un repas 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 553b2480c..435d4a92e 100644 --- a/source/rules/conventions-collectives/optique.yaml +++ b/source/rules/conventions-collectives/optique.yaml @@ -65,9 +65,10 @@ 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 points + par défaut: 110 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 e4fa0f749..cd89df0bb 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,7 +201,8 @@ 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é ? - par défaut: 5 jours + unité: jours + par défaut: 5 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 30106263b..57eca2f83 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,7 +206,8 @@ 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 - par défaut: 0 manifestations + unité: manifestations + par défaut: 0 contrat salarié . convention collective . sport . primes: titre: primes de manifestation @@ -225,7 +226,8 @@ 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 € + par défaut: 100 + unité: € contrat salarié . convention collective . sport . primes . manifestation 1 . franchise: titre: franchise manifestation 1 @@ -236,7 +238,8 @@ 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 € + par défaut: 100 + unité: € contrat salarié . convention collective . sport . primes . manifestation 2 . franchise: titre: franchise manifestation 2 @@ -247,7 +250,8 @@ 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 € + par défaut: 100 + unité: € contrat salarié . convention collective . sport . primes . manifestation 3 . franchise: titre: franchise manifestation 3 @@ -258,7 +262,8 @@ 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 € + par défaut: 100 + unité: € contrat salarié . convention collective . sport . primes . manifestation 4 . franchise: titre: franchise manifestation 4 @@ -269,7 +274,8 @@ 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 € + par défaut: 100 + unité: € contrat salarié . convention collective . sport . primes . manifestation 5 . franchise: titre: franchise manifestation 5 @@ -280,7 +286,8 @@ 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 € + par défaut: 100 + unité: € 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 2f4413329..b61094790 100644 --- a/source/rules/dirigeant.yaml +++ b/source/rules/dirigeant.yaml @@ -20,8 +20,6 @@ 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é @@ -99,10 +97,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é: €/mois + unité par défaut: €/mois dirigeant . auto-entrepreneur . cotisations et contributions: - unité: €/mois + unité par défaut: €/mois formule: somme: - cotisations @@ -159,7 +157,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é: €/mois + unité par défaut: €/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 @@ -329,7 +327,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é: €/an + unité par défaut: €/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 @@ -357,7 +355,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é: €/an + unité par défaut: €/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. @@ -429,6 +427,7 @@ 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 @@ -446,7 +445,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é: €/an + unité par défaut: €/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. @@ -494,7 +493,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 @@ -524,7 +523,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 @@ -645,13 +644,15 @@ 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 €/an + par défaut: 0 dirigeant . indépendant . cotisations et contributions . cotisations . déduction tabac . revenus déduits: titre: assiette des cotisations (avec déduction tabac) @@ -703,6 +704,7 @@ 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, @@ -712,10 +714,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 €/an + par défaut: 0 dirigeant . indépendant . contrats madelin . mutuelle . plafond: - unité: €/an + unité par défaut: €/an formule: somme: - produit: @@ -737,6 +739,7 @@ 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, @@ -746,10 +749,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 €/an + par défaut: 0 dirigeant . indépendant . contrats madelin . retraite . plafond: - unité: €/an + unité par défaut: €/an formule: le maximum de: - barème: @@ -779,11 +782,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' @@ -864,18 +867,23 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie dirigeant . indépendant . cotisations et contributions . cotisations . maladie . taux RSA: formule: - encadrement: - valeur: taux RSA part variable + 1.35% + produit: + assiette: 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 / (110% * plafond sécurité sociale temps plein) + 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% dirigeant . indépendant . cotisations et contributions . cotisations . maladie . taux: formule: @@ -960,7 +968,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 @@ -1084,9 +1092,10 @@ 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 €/an + par défaut: 0 dirigeant . indépendant . cotisations et contributions . CSG et CRDS . assiette: note: >- @@ -1252,7 +1261,8 @@ 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 €/an + par défaut: 0 + unité: €/an dirigeant . indépendant . IJSS . imposable: titre: indemnités journalières imposable @@ -1277,4 +1287,5 @@ 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 €/an + par défaut: 0 + unité: €/an diff --git a/source/rules/déclaration-revenu-indépendant.yaml b/source/rules/déclaration-revenu-indépendant.yaml index e38fc2779..f5259b1d8 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é: €/an + unité par défaut: €/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 83f19a7d2..b5a5e9b98 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é: €/an + unité par défaut: €/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é: €/an + unité par défaut: €/an formule: chiffre d'affaires entreprise . chiffre d'affaires de société: @@ -69,7 +69,8 @@ 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 ? - par défaut: 100% + unité: '%' + par défaut: 1 entreprise . bénéfice: formule: chiffre d'affaires - charges dont rémunération dirigeant @@ -121,7 +122,8 @@ 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 €/an + par défaut: 0 + unité par défaut: €/an entreprise . ACRE: description: >- @@ -157,7 +159,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 @@ -231,7 +233,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: @@ -245,10 +247,11 @@ 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 @@ -286,7 +289,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 @@ -343,7 +346,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 d9452fda8..716b0805a 100644 --- a/source/rules/impôt.yaml +++ b/source/rules/impôt.yaml @@ -35,7 +35,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 @@ -125,7 +125,7 @@ impôt . impôt sur le revenu: Une contribution sur les hauts revenus ajoute deux tranches supplémentaires. Attention : pour un revenu de 100 000€ annuels, le contribuable ne paiera 41 000€ d'impôt (le taux de la 4ème tranche est 41%) ! Ces 41% sont appliqués uniquement à la part de ses revenus supérieure à 72 617€. - unité: €/an + unité par défaut: €/an formule: barème: assiette: revenu abattu @@ -142,14 +142,14 @@ impôt . impôt sur le revenu: exemples: - nom: Haut salaire de ~ 10 000€ mensuels situation: - contrat salarié . rémunération . net imposable: 10000 + contrat salarié . rémunération . net imposable: 120000 valeur attendue: 30227 références: Article 197 du Code général des impôts: https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000006069577&idArticle=LEGIARTI000006308322 impôt . impôt sur le revenu à payer: description: Une décote est appliquée après le barème de l'impôt sur le revenu, pour réduire l'impôt des bas revenus. - unité: €/an + unité par défaut: €/an formule: allègement: assiette: impôt sur le revenu @@ -159,7 +159,7 @@ impôt . impôt sur le revenu à payer: exemples: - nom: Salaire d'un cadre situation: - contrat salarié . rémunération . net imposable: 48 k€/an + contrat salarié . rémunération . net imposable: 48000 valeur attendue: 6977 note: Le calcul utilisé pour la décôte concerne uniquement les foyers célibataires, le calcul est différent pour les couples. @@ -357,7 +357,7 @@ impôt . taux personnalisé: revenus net de cotisations: résumé: Avant impôt - unité: €/an + unité par défaut: €/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. @@ -368,7 +368,7 @@ revenus net de cotisations: - dirigeant . auto-entrepreneur . net de cotisations revenu net après impôt: - unité: €/an + unité par défaut: €/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 3b4ff4742..3cda86c32 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 = 0 + - si: trimestres validés par an = 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: +protection sociale . retraite . trimestres validés par an: unité: trimestres validés/an formule: somme: @@ -76,14 +76,14 @@ protection sociale . retraite . trimestres validés: - trimestres auto-entrepreneur plafond: 4 -protection sociale . retraite . trimestres validés . trimestres salarié: +protection sociale . retraite . trimestres validés par an . trimestres salarié: unité: trimestres validés/an applicable si: contrat salarié formule: barème trimestres générique -protection sociale . retraite . trimestres validés . trimestres indépendant: +protection sociale . retraite . trimestres validés par an . 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 . trimestres indépendant: valeur: barème trimestres générique plancher: 3 -protection sociale . retraite . trimestres validés . barème trimestres générique: +protection sociale . retraite . trimestres validés par an . barème trimestres générique: unité: trimestres validés/an formule: grille: @@ -112,8 +112,8 @@ protection sociale . retraite . trimestres validés . barème trimestres génér 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 . trimestres auto-entrepreneur: - applicable si: dirigeant . auto-entrepreneur +protection sociale . retraite . trimestres validés par an . 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 5601b8e45..7e0b21442 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,7 +130,8 @@ 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 titres-restaurant/mois + par défaut: 19 + unité: titres-restaurant/mois contrat salarié . frais professionnels . titres-restaurant . montant unitaire: question: Quelle est la valeur unitaire du titre-restaurant ? @@ -138,18 +139,20 @@ 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 €/titre-restaurant + par défaut: 8 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 % + par défaut: 50 + unité: '%' suggestions: 50%: 50 60%: 60 @@ -191,13 +194,14 @@ 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 km/mois + par défaut: 80 contrat salarié . activité partielle: question: Le salarié est-il en chômage partiel ? @@ -244,7 +248,8 @@ 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. - par défaut: 0 heures/mois + unité: heures/mois + par défaut: 0 suggestions: 30 h/semaine: 130 20 h/semaine: 86.6666 @@ -333,13 +338,12 @@ 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: - 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 + 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: @@ -446,28 +450,28 @@ contrat salarié . CDD . CPF: situation: CDD: non cotisations . assiette: 1480 - valeur attendue: false + valeur attendue: 0 - nom: SMIC situation: CDD: oui - événement: non - motif: "'accroissement activité'" + événement: aucun + motif: accroissement activité contrat jeune vacances: non cotisations . assiette: 1480 valeur attendue: 14.8 - nom: salaire médian situation: CDD: oui - événement: non - motif: "'accroissement activité'" + événement: aucun + 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: false + valeur attendue: 0 contrat salarié . CDD . compensation pour congés non pris: titre: indemnité de congés payés @@ -515,26 +519,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 €/mois + rémunération . brut de base: 2300 prime de fin de contrat: 0 congés non pris: 0 durée contrat: 12 - valeur attendue: false + valeur attendue: 0 - nom: 10 jours non pris situation: CDD: oui - rémunération . brut de base: 2300 €/mois + rémunération . brut de base: 2300 prime de fin de contrat: 0 - congés non pris: 10 jours ouvrés - durée contrat: 12 mois + congés non pris: 10 + durée contrat: 12 valeur attendue: 92 - nom: 3 jours non pris situation: CDD: oui - rémunération . brut de base: 2300 €/mois + rémunération . brut de base: 2300 prime de fin de contrat: 0 - congés non pris: 3 jours ouvrés - durée contrat: 6 mois + congés non pris: 3 + durée contrat: 6 valeur attendue: 55.21 note: | L'indemnité est versée à la fin du contrat, sauf si le CDD se poursuit par un CDI. @@ -609,14 +613,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: false + valeur attendue: 0 références: Code du travail - Article L1243-8: https://www.legifrance.gouv.fr/affichCode.do?idSectionTA=LEGISCTA000006189459&cidTexte=LEGITEXT000006072050 @@ -670,7 +674,8 @@ 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 % + par défaut: 2.22 + unité par défaut: '%' references: taux moyen national: https://www.legifrance.gouv.fr/affichTexteArticle.do;jsessionid=4702534627E4A8CF240B990E28C81AF4.tplgfr30s_3?idArticle=JORFARTI000033735834&cidTexte=JORFTEXT000033735824&dateTexte=29990101&categorieLien=id @@ -688,12 +693,11 @@ contrat salarié . CDD . événement: # elle apparaîtrait alors forcément _après_ la question du motif formule: une possibilité: - 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 + - 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: @@ -743,7 +747,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 @@ -754,15 +758,14 @@ contrat salarié . CDD . motif . classique: titre: motifs classiques formule: une possibilité: - possibilités: - - remplacement - - accroissement activité - - saisonnier - - usage - - mission + - 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 @@ -867,23 +870,25 @@ 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 mois + par défaut: 1 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 jour ouvré + par défaut: 0 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. @@ -925,7 +930,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 @@ -947,7 +952,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 @@ -1025,6 +1030,7 @@ 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 @@ -1066,7 +1072,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é: €/mois + unité par défaut: €/mois suggestions: salaire médian: 2300 SMIC: 1539 @@ -1106,9 +1112,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 @@ -1118,7 +1124,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é: €/mois + unité par défaut: €/mois formule: brut de base / temps de travail . quotité de travail suggestions: salaire médian: 2300 @@ -1128,8 +1134,7 @@ 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 . heures supplémentaires: - titre: taux horaire (heure supplémentaire) +contrat salarié . rémunération . taux horaire des heures supplémentaires et complémentaires: 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 @@ -1186,6 +1191,7 @@ 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: @@ -1212,8 +1218,9 @@ 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 €/mois + par défaut: 0 contrat salarié . rémunération . primes . activité . conventionnelles: formule: 0 €/mois @@ -1253,6 +1260,7 @@ 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 @@ -1275,9 +1283,11 @@ 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 . heures supplémentaires + assiette: taux horaire des heures supplémentaires et complémentaires facteur: somme: - temps de travail . heures supplémentaires @@ -1286,9 +1296,11 @@ 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 . heures supplémentaires + assiette: taux horaire des heures supplémentaires et complémentaires facteur: somme: - temps de travail . heures complémentaires @@ -1367,9 +1379,10 @@ 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 €/mois + par défaut: 0 suggestions: 🚗 véhicule: 260 + unité par défaut: €/mois contrat salarié . rémunération . avantages en nature . ntic . montant: titre: outils NTIC @@ -1394,7 +1407,9 @@ 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 ? - par défaut: 800 € + unité: € + + par défaut: 800 # TODO : vérifier et documenter les chiffres suggestions: 📱: 400 @@ -1404,7 +1419,9 @@ 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 ? - par défaut: 20 €/mois + unité: €/mois + + par défaut: 20 suggestions: aucun: 0 standard: 20 @@ -1421,17 +1438,24 @@ 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 [ref repas forfaitaire]: 4.85 €/repas + assiette: montant forfaitaire d'un 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 repas/mois + par défaut: 21 + unité: repas/mois suggestions: 1 par jour: 21 2 par jour: 42 @@ -1494,6 +1518,7 @@ 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 @@ -1566,6 +1591,7 @@ 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: @@ -1617,7 +1643,7 @@ contrat salarié . prime d'impatriation: contrat salarié . rémunération . net: titre: Salaire net - unité: €/mois + unité par défaut: €/mois type: salaire question: Quel est votre salaire net ? résumé: Salaire net avant impôt @@ -1644,7 +1670,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é: €/mois + unité par défaut: €/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. @@ -1672,14 +1698,14 @@ contrat salarié . prix du travail: - rémunération . total - (- aides employeur) - médecine du travail - unité: €/mois + unité par défaut: €/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é: €/mois + unité par défaut: €/mois description: | C'est le total que l'employeur doit verser pour employer un salarié. formule: @@ -1711,7 +1737,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: @@ -1771,12 +1797,14 @@ contrat salarié . cotisations: contrat salarié . cotisations . salariales . conventionnelles: titre: cotisations salariales conventionnelles description: Cotisations spécifiques à la convention collective - formule: 0 €/mois + unité par défaut: €/mois + formule: 0 contrat salarié . cotisations . patronales . conventionnelles: titre: cotisations patronales conventionnelles description: Cotisations spécifiques à la convention collective - formule: 0 €/mois + unité par défaut: €/mois + formule: 0 contrat salarié . cotisations . maladie sur les revenus de remplacement: formule: @@ -1906,7 +1934,8 @@ 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 heures/semaine + par défaut: 32 + unité: heures/semaine question: Quel est le nombre d'heures travaillées par semaine dans le cadre du temps partiel ? contrôles: - si: @@ -1937,7 +1966,8 @@ 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 heure/mois + par défaut: 0 + unité: heure/mois suggestions: aucune: 0 39h / semaine: 17.33 @@ -1983,7 +2013,8 @@ 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 ? - par défaut: 0 heure/mois + unité: heure/mois + par défaut: 0 contrôles: - si: heures complémentaires > seuil légal niveau: information @@ -2429,10 +2460,11 @@ 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 @@ -2444,6 +2476,7 @@ 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. @@ -2463,7 +2496,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 €/mois + par défaut: 40 suggestions: basique: 40 élevé: 100 @@ -3040,6 +3073,7 @@ 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 @@ -3169,7 +3203,6 @@ 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. @@ -3184,7 +3217,7 @@ contrat salarié . taxe sur les salaires: - nom: non applicable par défaut situation: rémunération . brut de base: 2300 - valeur attendue: false + valeur attendue: 0 - nom: association non lucrative unipersonnelle situation: entreprise . association non lucrative: oui @@ -3368,8 +3401,7 @@ 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é: - applicable si: zone un - question: Votre entreprise appartient-elle à l'un des secteurs éligible LODEOM ? + question: Votre entreprise appartient-elle à l'un de ces secteurs ? 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 : @@ -3504,7 +3536,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 new file mode 100644 index 000000000..3d2a6737b --- /dev/null +++ b/source/selectors/analyseSelectors.ts @@ -0,0 +1,349 @@ +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 75e41a07e..492f51128 100644 --- a/source/selectors/companyStatusSelectors.ts +++ b/source/selectors/companyStatusSelectors.ts @@ -1,4 +1,4 @@ -import { SitePaths } from 'Components/utils/SitePathsContext' +import { SitePaths } from 'Components/utils/withSitePaths' import { add, any, diff --git a/source/selectors/ficheDePaieSelectors.ts b/source/selectors/ficheDePaieSelectors.ts new file mode 100644 index 000000000..d2b2a5daa --- /dev/null +++ b/source/selectors/ficheDePaieSelectors.ts @@ -0,0 +1,132 @@ +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 new file mode 100644 index 000000000..3527e34bb --- /dev/null +++ b/source/selectors/progressSelectors.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000..799850d52 --- /dev/null +++ b/source/selectors/repartitionSelectors.ts @@ -0,0 +1,88 @@ +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 deleted file mode 100644 index 127544573..000000000 --- a/source/selectors/simulationSelectors.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 f683db6da..c3195c7f7 100644 --- a/source/sites/mon-entreprise.fr/App.tsx +++ b/source/sites/mon-entreprise.fr/App.tsx @@ -1,23 +1,12 @@ import Route404 from 'Components/Route404' -import { - EngineProvider, - SituationProvider -} from 'Components/utils/EngineContext' -import { SitePathsContext } from 'Components/utils/SitePathsContext' -import Engine from 'Engine' +import { SitePathsContext } from 'Components/utils/withSitePaths' import 'iframe-resizer' import createRavenMiddleware from 'raven-for-redux' import Raven from 'raven-js' -import React, { useContext, useEffect, useMemo } from 'react' +import React, { useContext, useEffect } 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 { Rules } from 'Rules' -import { - configSituationSelector, - situationSelector -} from 'Selectors/simulationSelectors' import 'Ui/index.css' import Provider, { ProviderProps } from '../../Provider' import { @@ -70,19 +59,13 @@ const middlewares = [ type InFranceRouteProps = { basename: ProviderProps['basename'] language: ProviderProps['language'] - rules: Rules + rules: NonNullable['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: ['simulation'] })(store) + persistEverything({ except: ['rules', 'simulation'] })(store) persistSimulation(store) }} initialStore={{ ...retrievePersistedState(), - previousSimulation: retrievePersistedSimulation() + previousSimulation: retrievePersistedSimulation(), + rules }} > - - - + ) } -const Router = () => { +let RouterSwitch = () => { return ( <> {!inIframe() &&
    } @@ -123,67 +105,45 @@ const Router = () => { 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 560f8ba5d..ad7ed3f2b 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: 600; + font-weight: 500; } .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 c53a676b5..c90d90447 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 bd0d9e51d..fac62a808 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 ba1c47ac9..30216bbb9 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 1b620ce6d..b5837350f 100644 --- a/source/sites/mon-entreprise.fr/middlewares/trackSimulatorActions.ts +++ b/source/sites/mon-entreprise.fr/middlewares/trackSimulatorActions.ts @@ -1,4 +1,7 @@ -import { situationSelector } from 'Selectors/simulationSelectors' +import { + currentQuestionSelector, + situationSelector +} from 'Selectors/analyseSelectors' import Tracker from 'Tracker' export default (tracker: Tracker) => { @@ -14,15 +17,14 @@ export default (tracker: Tracker) => { situationSelector(newState)[action.step] ]) - // TODO : add tracking in UI instead ? - // if (!currentQuestionSelector(newState)) { - // tracker.push([ - // 'trackEvent', - // 'Simulator', - // 'simulation completed', - // 'after ' + newState.simulation.foldedSteps.length + ' questions' - // ]) - // } + if (!currentQuestionSelector(newState)) { + tracker.push([ + 'trackEvent', + 'Simulator', + 'simulation completed', + 'after ' + newState.simulation.foldedSteps.length + ' questions' + ]) + } } if (action.type === 'SET_ACTIVE_TARGET_INPUT') { @@ -36,14 +38,14 @@ export default (tracker: Tracker) => { if ( action.type === 'UPDATE_SITUATION' || - action.type === 'UPDATE_TARGET_UNIT' + action.type === 'UPDATE_DEFAULT_UNIT' ) { tracker.push([ 'trackEvent', 'Simulator', 'update situation', - ...(action.type === 'UPDATE_TARGET_UNIT' - ? ['unité', action.targetUnit] + ...(action.type === 'UPDATE_DEFAULT_UNIT' + ? ['unité', action.defaultUnit] : [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 d98a25e3a..028aa59d2 100644 --- a/source/sites/mon-entreprise.fr/pages/Coronavirus.tsx +++ b/source/sites/mon-entreprise.fr/pages/Coronavirus.tsx @@ -5,19 +5,19 @@ import chomagePartielConfig from 'Components/simulationConfigs/chômage-partiel. 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 { EvaluatedRule } from 'Engine/types' +import { getRuleFromAnalysis } from 'Engine/ruleUtils' import React, { useContext, useEffect, useState } from 'react' import { Helmet } from 'react-helmet' import { Trans, useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useLocation } from 'react-router' +import { EvaluatedRule } from 'Rules' +import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' import styled from 'styled-components' import Animate from 'Ui/animate' -import { DottedName } from 'Rules' declare global { interface Window { @@ -108,22 +108,25 @@ 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 = 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( + 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( 'chômage partiel . coût employeur habituel' ) if ( - typeof net?.nodeValue !== 'number' || - typeof netHabituel?.nodeValue !== 'number' || - typeof totalEntreprise?.nodeValue !== 'number' || - typeof totalEntrepriseHabituel?.nodeValue !== 'number' + !net?.nodeValue || + !netHabituel?.nodeValue || + totalEntreprise?.nodeValue == null || + !totalEntrepriseHabituel?.nodeValue ) { return null } @@ -150,19 +153,17 @@ function ExplanationSection() { { ...net, additionalText: language === 'fr' && ( - + <> Soit{' '} {formatValue({ - nodeValue: - (net.nodeValue / netHabituel.nodeValue) * 100, + value: (net.nodeValue / netHabituel.nodeValue) * 100, unit: '%', - language: 'fr', - precision: 0 + maximumFractionDigits: 0 })} {' '} du revenu net - + ) } ], @@ -172,21 +173,20 @@ function ExplanationSection() { { ...totalEntreprise, additionalText: language === 'fr' && ( - + <> Soit{' '} {formatValue({ - nodeValue: + value: (totalEntreprise.nodeValue / totalEntrepriseHabituel.nodeValue) * 100, unit: '%', - language: 'fr', - precision: 0 + maximumFractionDigits: 0 })} {' '} du coût habituel - + ) } ] @@ -203,7 +203,7 @@ type ComparaisonTableProps = { } type Line = Array< - EvaluatedRule & { + EvaluatedRule & { additionalText?: React.ReactNode } > @@ -278,15 +278,15 @@ function ComparaisonTable({ rows: [head, ...body] }: ComparaisonTableProps) { ) } -function ValueWithLink(rule: EvaluatedRule) { +function ValueWithLink(rule: EvaluatedRule) { const { language } = useTranslation().i18n return ( {formatValue({ - nodeValue: rule.nodeValue as number, + value: rule.nodeValue as number, language, unit: '€', - precision: 0 + maximumFractionDigits: 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 e817573ba..bd867e016 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 4d069e57e..e17b61ebb 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 9b055e452..930a2ef31 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 eab706712..7a88e4356 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 ee0cee966..d0812a4d7 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 78728d224..5bccca196 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 acfd2c859..cfea9bba9 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 273a30d64..ff73eaccd 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 deleted file mode 100644 index 1e6d34cb7..000000000 --- a/source/sites/mon-entreprise.fr/pages/Documentation.tsx +++ /dev/null @@ -1,20 +0,0 @@ -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/components/Documentation/RulesList.css b/source/sites/mon-entreprise.fr/pages/Documentation/RulesList.css similarity index 100% rename from source/components/Documentation/RulesList.css rename to source/sites/mon-entreprise.fr/pages/Documentation/RulesList.css diff --git a/source/components/Documentation/RulesList.tsx b/source/sites/mon-entreprise.fr/pages/Documentation/RulesList.tsx similarity index 70% rename from source/components/Documentation/RulesList.tsx rename to source/sites/mon-entreprise.fr/pages/Documentation/RulesList.tsx index 1000aa410..4368b3a84 100644 --- a/source/components/Documentation/RulesList.tsx +++ b/source/sites/mon-entreprise.fr/pages/Documentation/RulesList.tsx @@ -1,12 +1,12 @@ import SearchBar from 'Components/SearchBar' -import React, { useContext } from 'react' +import React 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 = useContext(EngineContext).getParsedRules() + const rules = useSelector(parsedRulesSelector) return (

    diff --git a/source/sites/mon-entreprise.fr/pages/Documentation/index.tsx b/source/sites/mon-entreprise.fr/pages/Documentation/index.tsx new file mode 100644 index 000000000..234a6984a --- /dev/null +++ b/source/sites/mon-entreprise.fr/pages/Documentation/index.tsx @@ -0,0 +1,18 @@ +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 bf16db536..1d1bf5ac1 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,20 +1,22 @@ 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 = useEvaluation(simulationConfig.objectifs as Array) + const results: EvaluatedRule[] = simulationConfig.objectifs.map( + (dottedName: DottedName) => useRule(dottedName) + ) const onGoingComputation = !results.filter(node => node.nodeValue != null) .length return ( @@ -49,10 +51,10 @@ export function Results({ componentRef }: ResultsProp) { {r.nodeValue != null ? ( formatValue({ - nodeValue: r.nodeValue || 0, + value: r.nodeValue || 0, language: 'fr', unit: '€', - precision: 0 + maximumFractionDigits: 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 new file mode 100644 index 000000000..c9c50975d --- /dev/null +++ b/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx @@ -0,0 +1,120 @@ +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 975a9a864..9307ca50c 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,29 +3,27 @@ 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, - useContext -} from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' import { DottedName } from 'Rules' -import { situationSelector } from 'Selectors/simulationSelectors' +import { + nextStepsSelector, + parsedRulesSelector, + ruleAnalysisSelector, + situationSelector +} from 'Selectors/analyseSelectors' 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' @@ -57,8 +55,7 @@ const lauchComputationWhenResultsInViewport = () => { export default function AideDéclarationIndépendant() { const dispatch = useDispatch() - const rules = useContext(EngineContext).getParsedRules() - + const rules = useSelector(parsedRulesSelector) const company = useSelector( (state: RootState) => state.inFranceApp.existingCompany ) @@ -217,9 +214,9 @@ function SubSection({ dottedName: sectionDottedName, hideTitle = false }: SubSectionProp) { - const parsedRules = useContext(EngineContext).getParsedRules() - const ruleTitle = parsedRules[sectionDottedName]?.title - const nextSteps = useNextQuestions() + const parsedRules = useSelector(parsedRulesSelector) + const ruleTitle = useRule(sectionDottedName)?.title + const nextSteps = useSelector(nextStepsSelector) const situation = useSelector(situationSelector) const title = hideTitle ? null : ruleTitle const subQuestions = Object.values(parsedRules).filter( @@ -246,8 +243,10 @@ type SimpleFieldProps = { } function SimpleField({ dottedName, question, summary }: SimpleFieldProps) { const dispatch = useDispatch() - const evaluatedRule = useEvaluation(dottedName) - const rules = useContext(EngineContext).getParsedRules() + const evaluatedRule = useSelector((state: RootState) => { + return ruleAnalysisSelector(state, { dottedName }) + }) + const rules = useSelector(parsedRulesSelector) 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 07deec910..bcbfe9f6c 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 10a9883a9..d84fc7c3f 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 0d9996ca2..260b6dd40 100644 --- a/source/sites/mon-entreprise.fr/pages/Gérer/index.tsx +++ b/source/sites/mon-entreprise.fr/pages/Gérer/index.tsx @@ -1,10 +1,11 @@ import { ScrollToTop } from 'Components/utils/Scroll' -import { SitePathsContext } from 'Components/utils/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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' @@ -15,14 +16,28 @@ export default function Gérer() { return ( <> - - ← Retour à mon activité - +
    + {location.pathname === + '/gérer/aide-declaration-independants/récapitulatif' ? ( + + ← Retour à ma déclaration + + ) : ( + + ← 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 3886b56ab..500b1f171 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 947ee94b7..1dbb0bc7b 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 6ac90d094..d4921701b 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 c936b2a45..ed6559fc3 100644 --- a/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx +++ b/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx @@ -1,21 +1,33 @@ import { setSimulationConfig, updateSituation } from 'Actions/actions' import { DistributionBranch } from 'Components/Distribution' -import { Condition } from 'Components/EngineValue' +import RuleLink from 'Components/RuleLink' import SimulateurWarning from 'Components/SimulateurWarning' import config from 'Components/simulationConfigs/artiste-auteur.yaml' import 'Components/TargetSelection.css' import { IsEmbeddedContext } from 'Components/utils/embeddedContext' -import { EngineContext, useEvaluation } from 'Components/utils/EngineContext' -import Value from 'Components/EngineValue' +import { formatValue } from 'Engine/format' import RuleInput from 'Engine/RuleInput' +import { getRuleFromAnalysis } from 'Engine/ruleUtils' import React, { createContext, useContext, useEffect, useState } from 'react' -import { Trans } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' +import { RootState } from 'Reducers/rootReducer' import { DottedName } from 'Rules' -import { situationSelector } from 'Selectors/simulationSelectors' +import { + analysisWithDefaultsSelector, + parsedRulesSelector, + ruleAnalysisSelector, + situationSelector +} from 'Selectors/analyseSelectors' 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) @@ -65,12 +77,16 @@ type SimpleFieldProps = { } function SimpleField({ dottedName }: SimpleFieldProps) { + const rule = useSelector(parsedRulesSelector)[dottedName] const dispatch = useDispatch() - const rule = useEvaluation(dottedName) + const analysis = useSelector((state: RootState) => { + return ruleAnalysisSelector(state, { dottedName }) + }) const initialRender = useContext(InitialRenderContext) - const parsedRules = useContext(EngineContext).getParsedRules() - const value = useSelector(situationSelector)[dottedName] ?? rule['par défaut'] - if (rule.isApplicable === false || rule.isApplicable === null) { + const parsedRules = useSelector(parsedRulesSelector) + const value = useSelector(situationSelector)[dottedName] + + if (!analysis.isApplicable) { return null } @@ -131,7 +147,10 @@ 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) @@ -147,15 +166,16 @@ function CotisationsResult() { Montant des cotisations - + + {formatValue({ + value: cotisationRule.nodeValue, + language: i18n.language, + unit: '€', + maximumFractionDigits: 0 + })} + - - - + {cotisationRule.nodeValue ? : null} ) } @@ -180,10 +200,9 @@ const branches = [ ] as const function RepartitionCotisations() { - const engine = useContext(EngineContext) const cotisations = branches.map(branch => ({ ...branch, - value: engine.evaluate(branch.dottedName).nodeValue as number + value: useRule(branch.dottedName).nodeValue as number })) const maximum = Math.max(...cotisations.map(x => x.value)) const total = cotisations.map(x => x.value).reduce((a = 0, b) => a + b) diff --git a/source/sites/mon-entreprise.fr/pages/Simulateurs/AutoEntrepreneur.tsx b/source/sites/mon-entreprise.fr/pages/Simulateurs/AutoEntrepreneur.tsx index a9e76da9b..96d705564 100644 --- a/source/sites/mon-entreprise.fr/pages/Simulateurs/AutoEntrepreneur.tsx +++ b/source/sites/mon-entreprise.fr/pages/Simulateurs/AutoEntrepreneur.tsx @@ -5,12 +5,13 @@ 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 { EngineContext } from 'Components/utils/EngineContext' +import { getRuleFromAnalysis } from 'Engine/ruleUtils' import { default as React, useContext } from 'react' import { Helmet } from 'react-helmet' import { Trans, useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useLocation } from 'react-router' +import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' export default function AutoEntrepreneur() { const dispatch = useDispatch() @@ -53,11 +54,12 @@ export default function AutoEntrepreneur() { } function ExplanationSection() { - const engine = useContext(EngineContext) + const analysis = useSelector(analysisWithDefaultsSelector) + const getRule = getRuleFromAnalysis(analysis) const { t } = useTranslation() const { palettes } = useContext(ThemeColorsContext) - const impôt = engine.evaluate('impôt') + const impôt = getRule('impôt') return (

    @@ -66,9 +68,7 @@ function ExplanationSection() { { const dispatch = useDispatch() const location = useLocation<{ fromGérer?: boolean }>() - const sitePaths = useContext(SitePathsContext) dispatch(setSimulationConfig(salariéConfig, location.state?.fromGérer)) + const sitePaths = useContext(SitePathsContext) return ( <> diff --git a/source/sites/mon-entreprise.fr/pages/integration/Options.tsx b/source/sites/mon-entreprise.fr/pages/integration/Options.tsx index a9f55561e..8a0f886f8 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 19030abca..26d6bde0e 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 c02c9c6e9..a959ceea4 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/SitePathsContext' -import Value from 'Components/EngineValue' +import { SitePathsContext } from 'Components/utils/withSitePaths' +import Value from 'Components/Value' import React, { useContext } from 'react' import emoji from 'react-easy-emoji' import { Trans, useTranslation } from 'react-i18next' @@ -14,7 +14,6 @@ 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: { @@ -111,12 +110,9 @@ export default function Activité({ defaultChecked={seuilRevenus === 'AUCUN'} />{' '} inférieurs à{' '} - {formatValue({ - nodeValue: activité['seuil déclaration'], - precision: 0, - language, - unit: '€' - })} + + {activité['seuil déclaration']} + )} @@ -129,12 +125,9 @@ export default function Activité({ defaultChecked={seuilRevenus === 'IMPOSITION'} />{' '} inférieurs à{' '} - {formatValue({ - nodeValue: activité['seuil pro'], - precision: 0, - language, - unit: '€' - })} + + {activité['seuil pro']} + {activité['seuil régime général'] && ( @@ -149,12 +142,9 @@ export default function Activité({ } />{' '} supérieurs à{' '} - {formatValue({ - nodeValue: activité['seuil pro'], - precision: 0, - language, - unit: '€' - })} + + {activité['seuil pro']} + )} @@ -170,13 +160,9 @@ export default function Activité({ } />{' '} supérieurs à{' '} - {formatValue({ - nodeValue: - activité['seuil régime général'] || activité['seuil pro'], - precision: 0, - language, - unit: '€' - })} + + {activité['seuil régime général'] || activité['seuil pro']} + 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 619187011..a1fbe8810 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 06d3df4ea..8061a2d9a 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 8b97f6101..3476fff1b 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 d06e3398b..6b9570bed 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/SitePathsContext' +import { SitePathsContext } from 'Components/utils/withSitePaths' 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 346ff3e28..04129e035 100644 --- a/source/sites/mon-entreprise.fr/sitePaths.ts +++ b/source/sites/mon-entreprise.fr/sitePaths.ts @@ -93,6 +93,10 @@ 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 eead76968..86fe3bd5c 100644 --- a/source/sites/publi.codes/Studio.tsx +++ b/source/sites/publi.codes/Studio.tsx @@ -1,13 +1,12 @@ // 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 = ` @@ -84,6 +83,7 @@ export default function Studio() { setEditorValue(newValue ?? '')} options={{ @@ -96,15 +96,9 @@ export default function Studio() { flex: 1; `} > - - {/* TODO: prévoir de changer la signature de EngineProvider */} - - - + + +

    ) @@ -112,23 +106,32 @@ export default function Studio() { type ResultsProps = { targets: string[] - rules: string onClickShare: React.MouseEventHandler } -export const Results = ({ targets, onClickShare, rules }: ResultsProps) => { +export const Results = ({ targets, onClickShare }: ResultsProps) => { const [rule, setCurrentTarget] = useState() const currentTarget = rule ?? (last(targets) as string) - const engine = useMemo(() => new Engine(rules), [rules]) + const error = Engine.useError() // 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 evaluation = engine.evaluate(currentTarget) + const analysis = Engine.useEvaluation(currentTarget) console.warn = originalWarn - return ( + return error !== null ? ( +
    + {nl2br(error)} +
    + ) : ( <>
    { {nl2br(warning)}
    ))} - {evaluation ? ( + {analysis ? (

    Résultats

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

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


    - {'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' - })} - {' '} -
    -
    - ))} + {analysis.temporalValue + ?.filter(({ value }) => value !== false) + .map(({ start: du, end: au, value }) => ( + + + Du {du} au {au} :{' '} + + {formatValue({ value, unit: analysis.unit })}{' '} +
    +
    + ))} )}
    @@ -240,41 +232,13 @@ 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; - > :first-child { - width: 100% !important; + section { + width: 100%; } } ` - -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 48dcdd6a2..803b112a7 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(rawRules) + const engine = new Engine({ rules: rawRules }) let controls = engine.setSituation({ brut: 2000000 }).controls() expect( controls.find( diff --git a/test/conversation.test.js b/test/conversation.test.js index 8363d04eb..b92344a78 100644 --- a/test/conversation.test.js +++ b/test/conversation.test.js @@ -1,25 +1,102 @@ import { expect } from 'chai' -import { getNextQuestions } from '../source/components/utils/useNextQuestion' -import Engine from 'Engine' +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: [] } +} describe('conversation', function() { it('should start with the first missing variable', function() { - const missingVariables = new Engine({ + 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 = { // TODO - this won't work without the indirection, figure out why - 'top . startHere': { formule: { somme: ['a', 'b'] } }, + 'top . startHere': { + formule: { somme: ['a', 'b', 'c'] } + }, 'top . a': { formule: 'aa' }, 'top . b': { formule: 'bb' }, + 'top . c': { formule: 'cc' }, 'top . aa': { question: '?', titre: 'a', unité: '€' }, - 'top . bb': { question: '?', titre: 'b', unité: '€' } - }).evaluate('top . startHere').missingVariables - expect(getNextQuestions([missingVariables])[0]).to.equal('top . aa') + '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) }) + it('should first ask for questions without defaults, then those with defaults', function() { - const engine = new Engine({ + let rules = { net: { formule: 'brut - cotisation' }, brut: { - question: 'Quel est le salaire brut ?', - unité: '€/an' + question: 'Quel est le salaire brut ?' }, cotisation: { formule: { @@ -45,24 +122,47 @@ 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') - expect( - getNextQuestions([engine.evaluate('net').missingVariables])[0] - ).to.equal('brut') + let step2 = reducers( + assocPath(['simulation', 'situation', 'brut'], '2300', step1), + { + type: 'STEP_ACTION', + name: 'fold', + step: 'brut' + } + ) - 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') + 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() }) }) diff --git a/test/ficheDePaieSelector.test.js b/test/ficheDePaieSelector.test.js new file mode 100644 index 000000000..abd718d7d --- /dev/null +++ b/test/ficheDePaieSelector.test.js @@ -0,0 +1,70 @@ +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/missingVariables.test.js b/test/generateQuestions.test.js similarity index 82% rename from test/missingVariables.test.js rename to test/generateQuestions.test.js index 70f0e356d..789aa99d2 100644 --- a/test/missingVariables.test.js +++ b/test/generateQuestions.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai' import Engine from 'Engine' import rules from 'Rules' -import { getNextSteps } from '../source/components/utils/useNextQuestion' +import { getNextSteps } from '../source/engine/generateQuestions' describe('Missing variables', function() { it('should identify missing variables', function() { @@ -19,7 +19,8 @@ describe('Missing variables', function() { 'sum . evt . ko': {} } const result = Object.keys( - new Engine(rawRules).evaluate('sum . startHere').missingVariables + new Engine({ rules: rawRules }).evaluate('sum . startHere') + .missingVariables ) expect(result).to.include('sum . evt . ko') @@ -37,7 +38,8 @@ describe('Missing variables', function() { 'sum . evt . nyet': {} } const result = Object.keys( - new Engine(rawRules).evaluate('sum . startHere').missingVariables + new Engine({ rules: rawRules }).evaluate('sum . startHere') + .missingVariables ) expect(result).to.include('sum . evt . nyet') @@ -54,7 +56,8 @@ describe('Missing variables', function() { 'sum . trois': {} } const result = Object.keys( - new Engine(rawRules).evaluate('sum . startHere').missingVariables + new Engine({ rules: rawRules }).evaluate('sum . startHere') + .missingVariables ) expect(result).to.be.empty @@ -72,7 +75,8 @@ describe('Missing variables', function() { 'sum . trois': {} } const result = Object.keys( - new Engine(rawRules).evaluate('sum . startHere').missingVariables + new Engine({ rules: rawRules }).evaluate('sum . startHere') + .missingVariables ) expect(result).to.be.empty @@ -87,7 +91,8 @@ describe('Missing variables', function() { } } const result = Object.keys( - new Engine(rawRules).evaluate('top . startHere').missingVariables + new Engine({ rules: rawRules }).evaluate('top . startHere') + .missingVariables ) expect(result).to.include('top . trois') @@ -103,7 +108,8 @@ describe('Missing variables', function() { } } const result = Object.keys( - new Engine(rawRules).evaluate('top . startHere').missingVariables + new Engine({ rules: rawRules }).evaluate('top . startHere') + .missingVariables ) expect(result).to.be.empty @@ -119,8 +125,8 @@ describe('Missing variables', function() { } } const result = Object.keys( - new Engine(rawRules) - .setSituation({ 'top . trois': "'ko'" }) + new Engine({ rules: rawRules }) + .setSituation({ 'top . trois': 'ko' }) .evaluate('top . startHere').missingVariables ) @@ -171,7 +177,8 @@ describe('Missing variables', function() { 'top . quatre': {} } const result = Object.keys( - new Engine(rawRules).evaluate('top . startHere').missingVariables + new Engine({ rules: rawRules }).evaluate('top . startHere') + .missingVariables ) expect(result).to.include('top . dix') @@ -197,7 +204,7 @@ describe('nextSteps', function() { } const result = Object.keys( - new Engine(rawRules).evaluate('top . sum').missingVariables + new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables ) expect(result).to.have.lengthOf(1) @@ -216,7 +223,7 @@ describe('nextSteps', function() { } const result = Object.keys( - new Engine(rawRules).evaluate('top . sum').missingVariables + new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables ) expect(result).to.have.lengthOf(1) @@ -239,7 +246,7 @@ describe('nextSteps', function() { 'top . sum . evt . ko': {} } const result = Object.keys( - new Engine(rawRules).evaluate('top . sum').missingVariables + new Engine({ rules: rawRules }).evaluate('top . sum').missingVariables ) expect(result).to.eql(['top . sum . evt']) @@ -247,15 +254,13 @@ describe('nextSteps', function() { it('should ask "motif CDD" if "CDD" applies', function() { const result = Object.keys( - new Engine(rules) + new Engine({ rules, useDefaultValues: false }) .setSituation({ 'contrat salarié': 'oui', 'contrat salarié . CDD': 'oui', 'contrat salarié . rémunération . brut de base': '2300' }) - .evaluate('contrat salarié . rémunération . net', { - useDefaultValues: false - }).missingVariables + .evaluate('contrat salarié . rémunération . net').missingVariables ) expect(result).to.include('contrat salarié . CDD . motif') @@ -264,19 +269,19 @@ describe('nextSteps', function() { describe('getNextSteps', function() { it('should give priority to questions that advance most targets', function() { - let missingVariablesByTarget = [ - { + let missingVariablesByTarget = { + chargé: { effectif: 34.01, cadre: 30 }, - { + net: { cadre: 10.1 }, - { + aides: { effectif: 32.0, cadre: 10 } - ] + } let result = getNextSteps(missingVariablesByTarget) @@ -284,17 +289,17 @@ describe('getNextSteps', function() { }) it('should give priority to questions by total weight when advancing the same target count', function() { - let missingVariablesByTarget = [ - { + let missingVariablesByTarget = { + chargé: { effectif: 24.01, cadre: 30 }, - { + net: { effectif: 24.01, cadre: 10.1 }, - {} - ] + aides: {} + } let result = getNextSteps(missingVariablesByTarget) diff --git a/test/inversion.test.js b/test/inversion.test.js index bb6cb3438..97f851bda 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,7 +58,9 @@ 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) }) @@ -84,10 +86,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') @@ -125,7 +127,7 @@ describe('inversions', () => { taxe: formule: produit: - assiette: 1200 € + assiette: 1200 variations: - si: cadre alors: @@ -133,7 +135,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 @@ -174,7 +176,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) @@ -212,7 +214,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 67e150b76..6e09894ad 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 an existing rule base', function() { - let extraRules = ` + it.skip('should let the user add rules to the default ones', function() { + let rules = ` yo: formule: 1 ya: formule: contrat salarié . rémunération . net + yo ` - let engine = new Engine(rules, extraRules) + let engine = new Engine({ extra: rules }) 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(rules, sasuRules) + let engine = new Engine({ extra: 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] = [ + let [revenuDisponible, dividendes] = engine.evaluate([ '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(co2) + let engine = new Engine({ rules: 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 7001cd85e..d7137097a 100644 --- a/test/mecanisms.test.js +++ b/test/mecanisms.test.js @@ -8,21 +8,20 @@ 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(suite) + const engine = new Engine({ rules: suite, useDefaultValues: false }) describe(`Mécanisme ${suiteName}`, () => { Object.entries(suite) .filter(([, rule]) => rule?.exemples) .forEach(([name, test]) => { const { exemples, 'unité attendue': unit } = test - coerceArray(exemples).forEach( + exemples.forEach( ( { nom: testName, situation, - 'unité par défaut': defaultUnit, + 'unités par défaut': defaultUnits, 'valeur attendue': valeur, 'variables manquantes': expectedMissing }, @@ -38,10 +37,8 @@ testSuites.forEach(([suiteName, suite]) => { () => { const result = engine .setSituation(situation ?? {}) - .evaluate(name, { - unit: defaultUnit, - useDefaultValues: false - }) + .setDefaultUnits(defaultUnits) + .evaluate(name) if (typeof valeur === 'number') { expect(result.nodeValue).to.be.closeTo(valeur, 0.001) } else if (valeur !== undefined) { diff --git a/test/mécanismes/allègement.yaml b/test/mécanismes/allègement.yaml index 8831e12ce..490bb11ea 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 d85620ed8..4697071a0 100644 --- a/test/mécanismes/applicable.yaml +++ b/test/mécanismes/applicable.yaml @@ -1,4 +1,12 @@ 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 @@ -14,4 +22,4 @@ prévoyance obligatoire cadre: - nom: Non Applicabilité situation: statut cadre: non - valeur attendue: false + valeur attendue: 0 diff --git a/test/mécanismes/conversion-unité.yaml b/test/mécanismes/conversion-unité.yaml index 31b2efed5..1c9bcdf83 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és par défaut prioritaire devant unité de variable + - nom: Unité de variable prioritaire devant les unités par défaut situation: douches par mois: 30 - unité par défaut: douche/mois - valeur attendue: 30 + unités par défaut: [douche/mois] + valeur attendue: 360 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é par défaut: kCo2/an + unités 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é par défaut: €/mois + unités 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é par défaut: €/mois + unités 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é par défaut: €/an - valeur attendue: 880 + - unités par défaut: [€/an] + valeur attendue: 880 Conversion dans avec un abattement en %: - unité: €/an + unité par défaut: €/an formule: allègement: assiette: 1000€/an @@ -196,10 +196,10 @@ Conversion avec plusieurs échelons: - prévoyance cadre - 35€/mois exemples: - unité par défaut: €/an - situation: - assiette mensuelle: 1100 - valeur attendue: 600 + - unités par défaut: [€/an] + situation: + assiette mensuelle: 1100 + valeur attendue: 600 Conversion de situation: formule: @@ -207,7 +207,25 @@ Conversion de situation: - retraite - mutuelle exemples: - unité par défaut: €/an - situation: - retraite: 4000 - valeur attendue: 4360 + - 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 diff --git a/test/mécanismes/date.yaml b/test/mécanismes/date.yaml index beef4a793..8b7710aa8 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: false + valeur attendue: 0 diff --git a/test/mécanismes/expressions.yaml b/test/mécanismes/expressions.yaml index 90612b300..586462bd1 100644 --- a/test/mécanismes/expressions.yaml +++ b/test/mécanismes/expressions.yaml @@ -172,8 +172,7 @@ négation: pourcentage: formule: 38.1% exemples: - - valeur attendue: 38.1 - unité attendue: '%' + - valeur attendue: 0.381 #- test: variable modifiée temporellement multiplication et pourcentage: @@ -214,10 +213,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: @@ -268,24 +267,6 @@ 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 2a287044e..09fe34574 100644 --- a/test/mécanismes/le-maximum-de.yaml +++ b/test/mécanismes/le-maximum-de.yaml @@ -9,7 +9,9 @@ Maximum: taux: 9% exemples: - - valeur attendue: 1 + - nom: + situation: + valeur attendue: 1 a: applicable si: non diff --git a/test/mécanismes/question-conditionelle.yaml b/test/mécanismes/question-conditionelle.yaml index 6dc0596ec..fbc3ec391 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 enfants + par défaut: 4 famille nombreuse: titre: question conditionnelle diff --git a/test/mécanismes/remplace.yaml b/test/mécanismes/remplace.yaml index 16a405534..b1fe273c0 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 car branche desactivée: +remplacement non applicable (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 0114a26ed..3b47d7679 100644 --- a/test/mécanismes/régularisation.yaml +++ b/test/mécanismes/régularisation.yaml @@ -88,9 +88,4 @@ 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: 0 - -régularisation . test variations 3: - formule: cotisation spéciale | du 01/03/2020 | au 31/03/2020 - exemples: - - valeur attendue: 1380 + - valeur attendue: 660 diff --git a/test/mécanismes/toutes-ces-conditions.yaml b/test/mécanismes/toutes-ces-conditions.yaml index ae07b7682..7ec5510e1 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: false + valeur attendue: 0 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 af37bb257..e651c38d0 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: false + valeur attendue: 0 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 e8519bf34..0c8d201ec 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 - (prix * 50% | du 01/01/2020 | au 31/01/2020) + formule: prix * (50% | du 01/01/2020 | au 31/01/2020) début: fin: variable temporelle numérique . expression . multiplication: @@ -96,17 +96,14 @@ 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: @@ -115,7 +112,7 @@ taux associé: alors: 10%/mois - si: prix avec variations < 20 €/mois alors: 60%/mois - # Cette formule peut paraître bizarre, mais lorsque le prix est non + # Cette formule peut paraître bizarre, mais lorsque multiplication 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 289e69206..930cb8c38 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: 5 + valeur attendue: 0.05 - 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: 1 + valeur attendue: 0.01 variations au sein d'un mécanisme: formule: diff --git a/test/real-rules.test.js b/test/real-rules.test.js index fdb0460f9..b1b2717dd 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(parsedRules) +const engine = new Engine({ rules: parsedRules }) let runExamples = (examples, rule) => examples.map(ex => { const expected = ex['valeur attendue'] @@ -19,9 +19,10 @@ let runExamples = (examples, rule) => ) const evaluation = engine .setSituation(situation) - .evaluate(rule.dottedName, { - unit: ex['unités par défaut']?.[0] ?? rule['unité par défaut'] - }) + .setDefaultUnits( + ex['unités par défaut'] ?? [rule['unité par défaut'] ?? '€/mois'] + ) + .evaluate(rule.dottedName) 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 6db17c2e5..dcb3975c5 100644 --- a/test/regressions/__snapshots__/simulations.jest.js.snap +++ b/test/regressions/__snapshots__/simulations.jest.js.snap @@ -1,453 +1,453 @@ // 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`] = `"[931]"`; +exports[`calculate simulations-artiste-auteur: bnc 3`] = `"[932]"`; -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,116,20000,0,20000]"`; +exports[`calculate simulations-auto-entrepreneur: ACRE 1`] = `"[21394,1394,20000,0,20000]"`; -exports[`calculate simulations-auto-entrepreneur: ACRE 2`] = `"[31029,86,30000,0,30000]"`; +exports[`calculate simulations-auto-entrepreneur: ACRE 2`] = `"[31029,1029,30000,0,30000]"`; -exports[`calculate simulations-auto-entrepreneur: ACRE 3`] = `"[44304,359,40000,0,40000]"`; +exports[`calculate simulations-auto-entrepreneur: ACRE 3`] = `"[44304,4304,40000,0,40000]"`; -exports[`calculate simulations-auto-entrepreneur: aides 1`] = `"[5348,29,5000,0,5000]"`; +exports[`calculate simulations-auto-entrepreneur: aides 1`] = `"[5348,348,5000,0,5000]"`; -exports[`calculate simulations-auto-entrepreneur: aides 2`] = `"[53485,290,50000,93,49907]"`; +exports[`calculate simulations-auto-entrepreneur: aides 2`] = `"[53485,3485,50000,93,49907]"`; -exports[`calculate simulations-auto-entrepreneur: impôt sur le revenu 1`] = `"[32092,591,25000,706,24294]"`; +exports[`calculate simulations-auto-entrepreneur: impôt sur le revenu 1`] = `"[32092,7092,25000,706,24294]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 1`] = `"[574,6,500,0,500]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 1`] = `"[574,74,500,0,500]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 2`] = `"[1148,12,1000,0,1000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 2`] = `"[1148,148,1000,0,1000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 3`] = `"[2297,25,2000,0,2000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 3`] = `"[2297,297,2000,0,2000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 4`] = `"[5742,62,5000,0,5000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 4`] = `"[5742,742,5000,0,5000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 5`] = `"[11483,124,10000,0,10000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 5`] = `"[11483,1483,10000,0,10000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 6`] = `"[22966,247,20000,0,20000]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 6`] = `"[22966,2966,20000,0,20000]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 7`] = `"[57415,618,50000,275,49725]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 7`] = `"[57415,7415,50000,275,49725]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 8`] = `"[80381,865,70000,1340,68660]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 8`] = `"[80381,10381,70000,1340,68660]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 9`] = `"[114830,1236,100000,4008,95992]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 9`] = `"[114830,14830,100000,4008,95992]"`; -exports[`calculate simulations-auto-entrepreneur: échelle de revenus 10`] = `"[1148303,12359,1000000,131979,868021]"`; +exports[`calculate simulations-auto-entrepreneur: échelle de revenus 10`] = `"[1148303,148303,1000000,131979,868021]"`; -exports[`calculate simulations-indépendant: acre 1`] = `"[73024,23024,50000,51980,8052,41948,0,73024]"`; +exports[`calculate simulations-indépendant: acre 1`] = `"[73024,23024,50000,51980,8052,41948,null,73024]"`; -exports[`calculate simulations-indépendant: activité 1`] = `"[28923,8923,20000,20783,604,19396,0,28923]"`; +exports[`calculate simulations-indépendant: activité 1`] = `"[28923,8923,20000,20783,604,19396,null,28923]"`; -exports[`calculate simulations-indépendant: activité 2`] = `"[29101,9101,20000,20787,604,19396,0,29101]"`; +exports[`calculate simulations-indépendant: activité 2`] = `"[29101,9101,20000,20787,604,19396,null,29101]"`; -exports[`calculate simulations-indépendant: cotisations minimales 1`] = `"[1373,1273,100,134,0,100,0,1373]"`; +exports[`calculate simulations-indépendant: cotisations minimales 1`] = `"[1373,1273,100,134,0,100,null,1373]"`; -exports[`calculate simulations-indépendant: cotisations minimales 2`] = `"[245,145,100,104,0,100,0,245]"`; +exports[`calculate simulations-indépendant: cotisations minimales 2`] = `"[245,145,100,104,0,100,null,245]"`; -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 1`] = `"[29085,9085,20000,20787,603,19397,null,29085]"`; -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 2`] = `"[73024,23024,50000,51980,8213,41787,null,73024]"`; -exports[`calculate simulations-indépendant: impôt sur le revenu 3`] = `"[29085,9085,20000,20787,2079,17921,0,29085]"`; +exports[`calculate simulations-indépendant: impôt sur le revenu 3`] = `"[29085,9085,20000,20787,2079,17921,null,29085]"`; -exports[`calculate simulations-indépendant: inversions 1`] = `"[2000,1384,616,667,0,616,0,2000]"`; +exports[`calculate simulations-indépendant: inversions 1`] = `"[2000,1384,616,667,0,616,null,2000]"`; -exports[`calculate simulations-indépendant: inversions 2`] = `"[50000,16003,33997,35352,3563,30434,0,50000]"`; +exports[`calculate simulations-indépendant: inversions 2`] = `"[50000,16003,33997,35352,3563,30434,null,50000]"`; -exports[`calculate simulations-indépendant: inversions 3`] = `"[14596,4596,10000,10394,0,10000,0,14596]"`; +exports[`calculate simulations-indépendant: inversions 3`] = `"[14596,4596,10000,10394,0,10000,null,14596]"`; -exports[`calculate simulations-indépendant: inversions 4`] = `"[88547,27360,61187,63588,11187,50000,0,88547]"`; +exports[`calculate simulations-indépendant: inversions 4`] = `"[88547,27360,61187,63588,11187,50000,null,88547]"`; -exports[`calculate simulations-indépendant: inversions 5`] = `"[14596,4596,10000,10394,0,10000,1000,15596]"`; +exports[`calculate simulations-indépendant: inversions 5`] = `"[14596,4596,10000,10394,0,10000,null,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,0,1859]"`; +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 2`] = `"[2467,1467,1000,1064,0,1000,0,2467]"`; +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 3`] = `"[3075,1575,1500,1581,0,1500,0,3075]"`; +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 4`] = `"[3682,1682,2000,2097,0,2000,0,3682]"`; +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 5`] = `"[7427,2427,5000,5199,0,5000,0,7427]"`; +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 6`] = `"[14596,4596,10000,10394,0,10000,0,14596]"`; +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 7`] = `"[139594,39594,100000,103788,24245,75755,0,139594]"`; +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 8`] = `"[1239955,239955,1000000,1033666,467505,532495,0,1239955]"`; +exports[`calculate simulations-indépendant: échelle de revenus 8`] = `"[1239955,239955,1000000,1033666,467505,532495,null,1239955]"`; -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 1`] = `"[7257,7257,7184,4,13,16]"`; -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 2`] = `"[14963,14963,14544,4,26,32]"`; -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é - ACRE 3`] = `"[21575,22669,21905,4,40,48]"`; -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 1`] = `"[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 2`] = `"[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 3`] = `"[16490,16673,16178,4,29,35]"`; -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 4`] = `"[10982,10982,10742,4,19,23]"`; -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 5`] = `"[118288,173630,154362,4,46,203]"`; -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é - Contrats Madelin 6`] = `"[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 1`] = `"[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 2`] = `"[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 3`] = `"[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 4`] = `"[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é - activités 5`] = `"[10982,10982,10742,4,19,23]"`; -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 1`] = `"[5291,5291,5306,4,10,12]"`; -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é - avec charges 2`] = `"[10982,10982,10742,4,19,23]"`; -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 1`] = `"[-2313,-2313,2488,0,0,0]"`; -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 2`] = `"[169,169,139,0,1,1]"`; -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 3`] = `"[738,738,323,0,2,2]"`; -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 4`] = `"[2446,2446,2588,2,5,6]"`; -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 5`] = `"[5291,5291,5306,4,10,12]"`; -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 6`] = `"[10982,10982,10742,4,19,23]"`; -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 7`] = `"[25971,28055,27050,4,46,59]"`; -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: Assimilé salarié - échelle de rémunération 8`] = `"[46812,57017,52684,4,46,119]"`; -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 1`] = `"[9349,9349,2046,2,0,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 2`] = `"[18697,18697,4093,3,8,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 - ACRE 3`] = `"[28046,28046,6139,4,12,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 1`] = `"[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 2`] = `"[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 3`] = `"[24834,24834,8186,4,16,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 4`] = `"[17288,17288,4298,4,8,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 5`] = `"[235917,259318,39357,4,56,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 - Contrats Madelin 6`] = `"[17352,17352,4195,3,8,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 1`] = `"[15580,15580,6600,4,18,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 2`] = `"[15560,15560,0,4,0,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 3`] = `"[17336,17336,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 4`] = `"[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 - activités 5`] = `"[17417,17417,4093,3,8,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 1`] = `"[8450,8450,2456,3,0,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 - avec charges 2`] = `"[15480,15480,7163,4,14,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 1`] = `"[87,87,6,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 2`] = `"[871,871,60,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 3`] = `"[1742,1742,119,0,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 4`] = `"[4354,4354,1023,1,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 5`] = `"[8709,8709,2046,2,0,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 6`] = `"[17417,17417,4093,3,8,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 7`] = `"[43543,43543,10232,4,20,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: Auto-entrepreneur - échelle de rémunération 8`] = `"[84367,87085,20465,4,40,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 1`] = `"[8215,8215,6018,4,21,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 2`] = `"[16442,16527,12103,4,24,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 - ACRE 3`] = `"[20731,21570,15799,4,31,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 1`] = `"[19279,20567,15183,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 2`] = `"[18737,20264,15648,4,30,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 3`] = `"[19779,20620,15102,4,29,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 4`] = `"[13769,13769,10084,4,21,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 5`] = `"[144184,226878,57937,4,56,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 - Contrats Madelin 6`] = `"[13769,13769,10084,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 1`] = `"[13886,13886,10166,4,21,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 2`] = `"[14645,14645,0,4,0,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 3`] = `"[13758,13758,10075,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 4`] = `"[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 - activités 5`] = `"[13769,13769,10084,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 1`] = `"[6795,6795,4977,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 - avec charges 2`] = `"[13769,13769,10084,4,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 1`] = `"[-1044,-1044,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 2`] = `"[-225,-225,0,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 3`] = `"[616,616,470,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 4`] = `"[3084,3084,2267,3,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 5`] = `"[6795,6795,4977,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 6`] = `"[13769,13769,10084,4,21,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 7`] = `"[30434,33997,24912,4,48,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-rémunération-dirigeant: Indépendant - échelle de rémunération 8`] = `"[56273,69895,36431,4,56,0]"`; -exports[`calculate simulations-salarié: JEI 1`] = `"[3440,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: JEI 1`] = `"[3440,0,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: JEI 2`] = `"[26635,0,20000,15969,10681]"`; +exports[`calculate simulations-salarié: JEI 2`] = `"[26635,0,0,20000,15969,10681]"`; -exports[`calculate simulations-salarié: JEI 3`] = `"[4517,0,4000,3141,2741]"`; +exports[`calculate simulations-salarié: JEI 3`] = `"[4517,0,0,4000,3141,2741]"`; -exports[`calculate simulations-salarié: activité partielle 1`] = `"[27,0,1560,1197,1197]"`; +exports[`calculate simulations-salarié: activité partielle 1`] = `"[27,1218,0,1560,1197,1197]"`; -exports[`calculate simulations-salarié: activité partielle 2`] = `"[27,0,4000,2594,2365]"`; +exports[`calculate simulations-salarié: activité partielle 2`] = `"[27,2800,0,4000,2594,2365]"`; -exports[`calculate simulations-salarié: activité partielle 3`] = `"[778,0,8000,5209,4253]"`; +exports[`calculate simulations-salarié: activité partielle 3`] = `"[778,4849,0,8000,5209,4253]"`; -exports[`calculate simulations-salarié: activité partielle 4`] = `"[852,0,4000,2704,2444]"`; +exports[`calculate simulations-salarié: activité partielle 4`] = `"[852,2240,0,4000,2704,2444]"`; -exports[`calculate simulations-salarié: activité partielle 5`] = `"[2483,0,4000,2870,2562]"`; +exports[`calculate simulations-salarié: activité partielle 5`] = `"[2483,1400,0,4000,2870,2562]"`; -exports[`calculate simulations-salarié: activité partielle 6`] = `"[27,3750,3000,1940,1848]"`; +exports[`calculate simulations-salarié: activité partielle 6`] = `"[27,2100,3750,3000,1940,1848]"`; -exports[`calculate simulations-salarié: activité partielle 7`] = `"[27,0,4000,2594,2497]"`; +exports[`calculate simulations-salarié: activité partielle 7`] = `"[27,2800,0,4000,2594,2497]"`; -exports[`calculate simulations-salarié: activité partielle 8`] = `"[227,0,2000,1578,1547]"`; +exports[`calculate simulations-salarié: activité partielle 8`] = `"[227,1400,0,2000,1578,1547]"`; -exports[`calculate simulations-salarié: activité partielle 9`] = `"[1156,0,2000,1540,1510]"`; +exports[`calculate simulations-salarié: activité partielle 9`] = `"[1156,700,0,2000,1540,1510]"`; -exports[`calculate simulations-salarié: activité partielle 10`] = `"[327,0,6000,4182,3511]"`; +exports[`calculate simulations-salarié: activité partielle 10`] = `"[327,4200,0,6000,4182,3511]"`; -exports[`calculate simulations-salarié: aides 1`] = `"[2302,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: aides 1`] = `"[2302,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: aides 2`] = `"[12823,0,10000,8911,7667]"`; +exports[`calculate simulations-salarié: aides 2`] = `"[12823,0,0,10000,8911,7667]"`; -exports[`calculate simulations-salarié: aides 3`] = `"[2062,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: aides 3`] = `"[2062,417,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: aides 4`] = `"[2292,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: aides 4`] = `"[2292,208,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: apprentissage 1`] = `"[1552,0,1500,1448,1448]"`; +exports[`calculate simulations-salarié: apprentissage 1`] = `"[1552,0,0,1500,1448,1448]"`; -exports[`calculate simulations-salarié: apprentissage 2`] = `"[1385,0,1500,1448,1448]"`; +exports[`calculate simulations-salarié: apprentissage 2`] = `"[1385,167,0,1500,1448,1448]"`; -exports[`calculate simulations-salarié: assimilé salarié 1`] = `"[7015,0,5000,3943,3318]"`; +exports[`calculate simulations-salarié: assimilé salarié 1`] = `"[7015,0,0,5000,3943,3318]"`; -exports[`calculate simulations-salarié: assimilé salarié 2`] = `"[1583,0,1500,1163,1163]"`; +exports[`calculate simulations-salarié: assimilé salarié 2`] = `"[1583,0,0,1500,1163,1163]"`; -exports[`calculate simulations-salarié: assimilé salarié 3`] = `"[3685,0,3000,2348,2172]"`; +exports[`calculate simulations-salarié: assimilé salarié 3`] = `"[3685,0,0,3000,2348,2172]"`; -exports[`calculate simulations-salarié: atmp 1`] = `"[2534,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: atmp 1`] = `"[2534,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: avantages 1`] = `"[2667,0,2000,1540,1492]"`; +exports[`calculate simulations-salarié: avantages 1`] = `"[2667,0,0,2000,1540,1492]"`; -exports[`calculate simulations-salarié: avantages 2`] = `"[2677,0,2000,1539,1490]"`; +exports[`calculate simulations-salarié: avantages 2`] = `"[2677,0,0,2000,1539,1490]"`; -exports[`calculate simulations-salarié: avantages 3`] = `"[2587,0,2000,1549,1506]"`; +exports[`calculate simulations-salarié: avantages 3`] = `"[2587,0,0,2000,1549,1506]"`; -exports[`calculate simulations-salarié: cadre 1`] = `"[4122,0,3000,2348,2171]"`; +exports[`calculate simulations-salarié: cadre 1`] = `"[4122,0,0,3000,2348,2171]"`; -exports[`calculate simulations-salarié: cdd 1`] = `"[2509,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: cdd 1`] = `"[2509,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: cdd 2`] = `"[2591,0,2000,1599,1557]"`; +exports[`calculate simulations-salarié: cdd 2`] = `"[2591,0,0,2000,1599,1557]"`; -exports[`calculate simulations-salarié: cdd 3`] = `"[3394,0,2400,1967,1883]"`; +exports[`calculate simulations-salarié: cdd 3`] = `"[3394,0,0,2400,1967,1883]"`; -exports[`calculate simulations-salarié: effectif 1`] = `"[2479,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: effectif 1`] = `"[2479,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: effectif 2`] = `"[2525,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: effectif 2`] = `"[2525,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: effectif 3`] = `"[2539,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: effectif 3`] = `"[2539,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: effectif 4`] = `"[2539,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: effectif 4`] = `"[2539,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: frais pro - DFS 1`] = `"[2242,0,2000,1630,1630]"`; +exports[`calculate simulations-salarié: frais pro - DFS 1`] = `"[2242,0,0,2000,1630,1630]"`; -exports[`calculate simulations-salarié: frais pro - DFS 2`] = `"[2335,0,2000,1584,1544]"`; +exports[`calculate simulations-salarié: frais pro - DFS 2`] = `"[2335,0,0,2000,1584,1544]"`; -exports[`calculate simulations-salarié: frais pro - DFS 3`] = `"[2265,0,2000,1606,1563]"`; +exports[`calculate simulations-salarié: frais pro - DFS 3`] = `"[2265,0,0,2000,1606,1563]"`; -exports[`calculate simulations-salarié: frais pro - DFS 4`] = `"[2243,0,2000,1613,1569]"`; +exports[`calculate simulations-salarié: frais pro - DFS 4`] = `"[2243,0,0,2000,1613,1569]"`; -exports[`calculate simulations-salarié: frais pro - DFS 5`] = `"[2437,0,2000,1590,1590]"`; +exports[`calculate simulations-salarié: frais pro - DFS 5`] = `"[2437,0,0,2000,1590,1590]"`; -exports[`calculate simulations-salarié: frais pro - DFS 6`] = `"[1767,0,1700,1364,1364]"`; +exports[`calculate simulations-salarié: frais pro - DFS 6`] = `"[1767,0,0,1700,1364,1364]"`; -exports[`calculate simulations-salarié: frais pro - DFS 7`] = `"[3287,0,2600,2125,2097]"`; +exports[`calculate simulations-salarié: frais pro - DFS 7`] = `"[3287,0,0,2600,2125,2097]"`; -exports[`calculate simulations-salarié: frais pro - IKV 1`] = `"[4367,0,3200,2530,2320]"`; +exports[`calculate simulations-salarié: frais pro - IKV 1`] = `"[4367,0,0,3200,2530,2320]"`; -exports[`calculate simulations-salarié: frais pro - IKV 2`] = `"[4346,0,3200,2511,2302]"`; +exports[`calculate simulations-salarié: frais pro - IKV 2`] = `"[4346,0,0,3200,2511,2302]"`; -exports[`calculate simulations-salarié: frais pro - IKV 3`] = `"[2774,0,2157,1685,1630]"`; +exports[`calculate simulations-salarié: frais pro - IKV 3`] = `"[2774,0,0,2157,1685,1630]"`; -exports[`calculate simulations-salarié: frais pro - titres restaurant 1`] = `"[2519,0,2000,1521,1484]"`; +exports[`calculate simulations-salarié: frais pro - titres restaurant 1`] = `"[2519,0,0,2000,1521,1484]"`; -exports[`calculate simulations-salarié: frais pro - titres restaurant 2`] = `"[4307,0,3000,2134,1949]"`; +exports[`calculate simulations-salarié: frais pro - titres restaurant 2`] = `"[4307,0,0,3000,2134,1949]"`; -exports[`calculate simulations-salarié: frais pro - titres restaurant 3`] = `"[2562,0,2000,1493,1456]"`; +exports[`calculate simulations-salarié: frais pro - titres restaurant 3`] = `"[2562,0,0,2000,1493,1456]"`; -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 1`] = `"[2583,0,0,2000,1636,1599]"`; -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 2`] = `"[3105,0,0,2000,2009,1965]"`; -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 3`] = `"[2654,0,0,2000,1636,1599]"`; -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 4`] = `"[2565,0,0,2000,1627,1590]"`; -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 5`] = `"[3025,0,0,2000,1970,1932]"`; -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 6`] = `"[3041,0,0,2000,1978,1939]"`; -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 7`] = `"[3336,0,2446,2000,1919,1889]"`; -exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 8`] = `"[3286,2286,2000,1889,1859]"`; +exports[`calculate simulations-salarié: heures supplémentaires et complémentaires 8`] = `"[3286,0,2286,2000,1889,1859]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 1`] = `"[4076,0,3000,2353,2168]"`; +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 2`] = `"[41834,0,30000,24227,14588]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 2`] = `"[41834,0,0,30000,24227,14588]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 3`] = `"[4111,0,3000,2353,2172]"`; +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 4`] = `"[3896,0,3000,2353,2252]"`; +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 5`] = `"[41834,0,30000,24227,14588]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 5`] = `"[41834,0,0,30000,24227,14588]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 6`] = `"[4076,0,3000,2626,2481]"`; +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 7`] = `"[41834,0,30000,26966,16204]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 7`] = `"[41834,0,0,30000,26966,16204]"`; -exports[`calculate simulations-salarié: impôt sur le revenu 8`] = `"[4076,0,3000,2353,2107]"`; +exports[`calculate simulations-salarié: impôt sur le revenu 8`] = `"[4076,0,0,3000,2353,2107]"`; -exports[`calculate simulations-salarié: inversions 1`] = `"[2000,0,1746,1360,1353]"`; +exports[`calculate simulations-salarié: inversions 1`] = `"[2000,0,0,1746,1360,1353]"`; -exports[`calculate simulations-salarié: inversions 2`] = `"[3474,0,2554,2000,1898]"`; +exports[`calculate simulations-salarié: inversions 2`] = `"[3474,0,0,2554,2000,1898]"`; -exports[`calculate simulations-salarié: inversions 3`] = `"[3679,0,2706,2120,2000]"`; +exports[`calculate simulations-salarié: inversions 3`] = `"[3679,0,0,2706,2120,2000]"`; -exports[`calculate simulations-salarié: lodeom 1`] = `"[1592,0,1521,1182,1182]"`; +exports[`calculate simulations-salarié: lodeom 1`] = `"[1592,0,0,1521,1182,1182]"`; -exports[`calculate simulations-salarié: lodeom 2`] = `"[2085,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: lodeom 2`] = `"[2085,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: lodeom 3`] = `"[3896,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: lodeom 3`] = `"[3896,0,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: lodeom 4`] = `"[5674,0,4000,3146,2759]"`; +exports[`calculate simulations-salarié: lodeom 4`] = `"[5674,0,0,4000,3146,2759]"`; -exports[`calculate simulations-salarié: lodeom 5`] = `"[7889,0,5500,4349,3625]"`; +exports[`calculate simulations-salarié: lodeom 5`] = `"[7889,0,0,5500,4349,3625]"`; -exports[`calculate simulations-salarié: lodeom compétitivité renforcée 1`] = `"[1592,0,1521,1182,1182]"`; +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 2`] = `"[2085,0,2000,1561,1524]"`; +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 3`] = `"[3444,0,3000,2353,2187]"`; +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 4`] = `"[5588,0,4000,3146,2759]"`; +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 5`] = `"[7889,0,5500,4349,3625]"`; +exports[`calculate simulations-salarié: lodeom compétitivité renforcée 5`] = `"[7889,0,0,5500,4349,3625]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 1`] = `"[1592,0,1521,1182,1182]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 1`] = `"[1592,0,0,1521,1182,1182]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 2`] = `"[2085,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 2`] = `"[2085,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 3`] = `"[3235,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 3`] = `"[3235,0,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 4`] = `"[4915,0,4000,3146,2759]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 4`] = `"[4915,0,0,4000,3146,2759]"`; -exports[`calculate simulations-salarié: lodeom innovation et croissance 5`] = `"[7889,0,5500,4349,3625]"`; +exports[`calculate simulations-salarié: lodeom innovation et croissance 5`] = `"[7889,0,0,5500,4349,3625]"`; -exports[`calculate simulations-salarié: stage 1`] = `"[507,0,500,500,500]"`; +exports[`calculate simulations-salarié: stage 1`] = `"[507,0,0,500,500,500]"`; -exports[`calculate simulations-salarié: stage 2`] = `"[2490,0,2000,1750,1750]"`; +exports[`calculate simulations-salarié: stage 2`] = `"[2490,0,0,2000,1750,1750]"`; -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 1`] = `"[1606,0,0,1521,1195,1195]"`; -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 2`] = `"[3423,0,0,2500,1979,1880]"`; -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 3`] = `"[1592,0,0,1521,1170,1170]"`; -exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 4`] = `"[3382,0,2500,1938,1844]"`; +exports[`calculate simulations-salarié: taux spécifiques retraite complémentaire 4`] = `"[3382,0,0,2500,1938,1844]"`; -exports[`calculate simulations-salarié: temps partiel 1`] = `"[2592,2188,2000,1561,1524]"`; +exports[`calculate simulations-salarié: temps partiel 1`] = `"[2592,0,2188,2000,1561,1524]"`; -exports[`calculate simulations-salarié: temps partiel 2`] = `"[2533,2500,1857,1448,1428]"`; +exports[`calculate simulations-salarié: temps partiel 2`] = `"[2533,0,2500,1857,1448,1428]"`; -exports[`calculate simulations-salarié: temps partiel 3`] = `"[1159,1750,1000,770,770]"`; +exports[`calculate simulations-salarié: temps partiel 3`] = `"[1159,0,1750,1000,770,770]"`; -exports[`calculate simulations-salarié: treizième mois 1`] = `"[3390,0,2300,1950,1856]"`; +exports[`calculate simulations-salarié: treizième mois 1`] = `"[3390,0,0,2300,1950,1856]"`; -exports[`calculate simulations-salarié: treizième mois 2`] = `"[3800,2965,2300,2186,2073]"`; +exports[`calculate simulations-salarié: treizième mois 2`] = `"[3800,0,2965,2300,2186,2073]"`; -exports[`calculate simulations-salarié: treizième mois 3`] = `"[3044,0,2300,1799,1726]"`; +exports[`calculate simulations-salarié: treizième mois 3`] = `"[3044,0,0,2300,1799,1726]"`; -exports[`calculate simulations-salarié: échelle de salaires 1`] = `"[130,0,100,57,57]"`; +exports[`calculate simulations-salarié: échelle de salaires 1`] = `"[130,0,0,100,57,57]"`; -exports[`calculate simulations-salarié: échelle de salaires 2`] = `"[284,0,250,176,176]"`; +exports[`calculate simulations-salarié: échelle de salaires 2`] = `"[284,0,0,250,176,176]"`; -exports[`calculate simulations-salarié: échelle de salaires 3`] = `"[541,0,500,374,374]"`; +exports[`calculate simulations-salarié: échelle de salaires 3`] = `"[541,0,0,500,374,374]"`; -exports[`calculate simulations-salarié: échelle de salaires 4`] = `"[799,0,750,572,572]"`; +exports[`calculate simulations-salarié: échelle de salaires 4`] = `"[799,0,0,750,572,572]"`; -exports[`calculate simulations-salarié: échelle de salaires 5`] = `"[1056,0,1000,770,770]"`; +exports[`calculate simulations-salarié: échelle de salaires 5`] = `"[1056,0,0,1000,770,770]"`; -exports[`calculate simulations-salarié: échelle de salaires 6`] = `"[1313,0,1250,968,968]"`; +exports[`calculate simulations-salarié: échelle de salaires 6`] = `"[1313,0,0,1250,968,968]"`; -exports[`calculate simulations-salarié: échelle de salaires 7`] = `"[1571,0,1500,1165,1165]"`; +exports[`calculate simulations-salarié: échelle de salaires 7`] = `"[1571,0,0,1500,1165,1165]"`; -exports[`calculate simulations-salarié: échelle de salaires 8`] = `"[2479,0,2000,1561,1524]"`; +exports[`calculate simulations-salarié: échelle de salaires 8`] = `"[2479,0,0,2000,1561,1524]"`; -exports[`calculate simulations-salarié: échelle de salaires 9`] = `"[3401,0,2500,1957,1861]"`; +exports[`calculate simulations-salarié: échelle de salaires 9`] = `"[3401,0,0,2500,1957,1861]"`; -exports[`calculate simulations-salarié: échelle de salaires 10`] = `"[4076,0,3000,2353,2187]"`; +exports[`calculate simulations-salarié: échelle de salaires 10`] = `"[4076,0,0,3000,2353,2187]"`; -exports[`calculate simulations-salarié: échelle de salaires 11`] = `"[5674,0,4000,3146,2759]"`; +exports[`calculate simulations-salarié: échelle de salaires 11`] = `"[5674,0,0,4000,3146,2759]"`; -exports[`calculate simulations-salarié: échelle de salaires 12`] = `"[7085,0,5000,3948,3336]"`; +exports[`calculate simulations-salarié: échelle de salaires 12`] = `"[7085,0,0,5000,3948,3336]"`; -exports[`calculate simulations-salarié: échelle de salaires 13`] = `"[14319,0,10000,7958,6080]"`; +exports[`calculate simulations-salarié: échelle de salaires 13`] = `"[14319,0,0,10000,7958,6080]"`; -exports[`calculate simulations-salarié: échelle de salaires 14`] = `"[28345,0,20000,15969,10681]"`; +exports[`calculate simulations-salarié: échelle de salaires 14`] = `"[28345,0,0,20000,15969,10681]"`; -exports[`calculate simulations-salarié: échelle de salaires 15`] = `"[128575,0,100000,87157,46271]"`; +exports[`calculate simulations-salarié: échelle de salaires 15`] = `"[128575,0,0,100000,87157,46271]"`; -exports[`calculate simulations-salarié: échelle de salaires 16`] = `"[1243819,0,1000000,896257,446123]"`; +exports[`calculate simulations-salarié: échelle de salaires 16`] = `"[1243819,0,0,1000000,896257,446123]"`; diff --git a/test/regressions/simulations-artiste-auteur.yaml b/test/regressions/simulations-artiste-auteur.yaml index 3e5123d3d..3778c90f2 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 €/an - - artiste-auteur . revenus . traitements et salaires: 10000 €/an - - artiste-auteur . revenus . traitements et salaires: 100000 €/an + - artiste-auteur . revenus . traitements et salaires: 1000 + - artiste-auteur . revenus . traitements et salaires: 10000 + - artiste-auteur . revenus . traitements et salaires: 100000 bnc: - - artiste-auteur . revenus . BNC . recettes: 10000 €/an - - artiste-auteur . revenus . BNC . recettes: 10000 €/an + - artiste-auteur . revenus . BNC . recettes: 10000 + - artiste-auteur . revenus . BNC . recettes: 10000 artiste-auteur . revenus . BNC . micro-bnc: non - - artiste-auteur . revenus . BNC . recettes: 10000 €/an + - artiste-auteur . revenus . BNC . recettes: 10000 artiste-auteur . revenus . BNC . micro-bnc: non - artiste-auteur . revenus . BNC . frais réels: 5000 €/an + artiste-auteur . revenus . BNC . frais réels: 5000 diff --git a/test/regressions/simulations-auto-entrepreneur.yaml b/test/regressions/simulations-auto-entrepreneur.yaml index 742bc6753..1a7e4d24a 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 €/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 + - 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 aides: - - dirigeant . auto-entrepreneur . net de cotisations: 5000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 5000 entreprise . ACRE: oui - - dirigeant . auto-entrepreneur . net de cotisations: 50000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 50000 entreprise . ACRE: oui impôt sur le revenu: - - dirigeant . auto-entrepreneur . net de cotisations: 25000 €/an - entreprise . catégorie d'activité: "'libérale'" + - dirigeant . auto-entrepreneur . net de cotisations: 25000 + entreprise . catégorie d'activité: 'libérale' dirigeant . auto-entrepreneur . impôt . versement libératoire: oui ACRE: - - dirigeant . auto-entrepreneur . net de cotisations: 20000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 20000 entreprise . date de création: 01/01/2020 entreprise . ACRE: oui - - dirigeant . auto-entrepreneur . net de cotisations: 30000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 30000 entreprise . date de création: 01/06/2019 entreprise . ACRE: oui - - dirigeant . auto-entrepreneur . net de cotisations: 40000 €/an + - dirigeant . auto-entrepreneur . net de cotisations: 40000 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 d94241bfe..56c77f8d1 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 €/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 + - 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 inversions: - - 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 + - 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 cotisations minimales: - - dirigeant . indépendant . revenu net de cotisations: 100 €/an - - dirigeant . indépendant . revenu net de cotisations: 100 €/an + - dirigeant . indépendant . revenu net de cotisations: 100 + - dirigeant . indépendant . revenu net de cotisations: 100 situation personnelle . RSA: oui activité: - - 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'" + - 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 acre: - - dirigeant . indépendant . revenu net de cotisations: 50000 €/an + - dirigeant . indépendant . revenu net de cotisations: 50000 entreprise . ACRE: true impôt sur le revenu: - - 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é'" + - 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é 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 104f87467..9ca07d898 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 €/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 + - 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 avec charges: - - dirigeant . rémunération totale: 10000 €/an + - dirigeant . rémunération totale: 10000 entreprise . charges: 2000 - - dirigeant . rémunération totale: 20000 €/an + - dirigeant . rémunération totale: 20000 entreprise . charges: 15000 ACRE: - - dirigeant . rémunération totale: 10000 €/an + - dirigeant . rémunération totale: 10000 entreprise . date de création: 01/01/2020 entreprise . ACRE: oui - - dirigeant . rémunération totale: 20000 €/an + - dirigeant . rémunération totale: 20000 entreprise . date de création: 01/01/2020 entreprise . ACRE: oui - - dirigeant . rémunération totale: 30000 €/an + - dirigeant . rémunération totale: 30000 entreprise . date de création: 01/06/2020 entreprise . ACRE: oui activités: - - 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'" + - 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 entreprise . catégorie d'activité . libérale règlementée: oui - - 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'" + - 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 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 €/an + - dirigeant . rémunération totale: 30000 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 €/an + - dirigeant . rémunération totale: 30000 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 €/an + - dirigeant . rémunération totale: 30000 entreprise . charges: 10000 dirigeant . indépendant . contrats madelin . mutuelle . montant: 1000 # Cas global madelin faible - - dirigeant . rémunération totale: 20000 €/an + - dirigeant . rémunération totale: 20000 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 €/an + - dirigeant . rémunération totale: 300000 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 €/an + - dirigeant . rémunération totale: 20000 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 b0639cd9d..52edf30e6 100644 --- a/test/regressions/simulations-salarié.yaml +++ b/test/regressions/simulations-salarié.yaml @@ -1,316 +1,316 @@ échelle de salaires: - - 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 + - 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 effectif: - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 entreprise . effectif: 10 - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 entreprise . effectif: 20 - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 entreprise . effectif: 50 - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 entreprise . effectif: 100 inversions: - - 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 + - contrat salarié . prix du travail: 2000 + - contrat salarié . rémunération . net: 2000 + - contrat salarié . rémunération . net après impôt: 2000 stage: - - 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 + - contrat salarié: stage + contrat salarié . rémunération . brut de base: 500 + - contrat salarié: stage + contrat salarié . rémunération . brut de base: 2000 apprentissage: - - 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'" + - 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 cadre: - - contrat salarié . rémunération . brut de base: 3000 €/mois + - contrat salarié . rémunération . brut de base: 3000 contrat salarié . statut cadre: oui cdd: - - 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 + contrat salarié . rémunération . brut de base: 2000 + - contrat salarié: CDD + contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié: CDD + contrat salarié . rémunération . brut de base: 2400 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . ATMP . taux collectif ATMP: 5 assimilé salarié: - - 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 + - dirigeant: assimilé salarié + contrat salarié . rémunération . brut de base: 5000 + - dirigeant: assimilé salarié + contrat salarié . rémunération . brut de base: 1500 entreprise . ACRE: oui - - dirigeant: "'assimilé salarié'" - contrat salarié . rémunération . brut de base: 3000 €/mois + - dirigeant: assimilé salarié + contrat salarié . rémunération . brut de base: 3000 entreprise . ACRE: oui aides: - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . statut JEI: oui - - contrat salarié . rémunération . brut de base: 10000 €/mois + - contrat salarié . rémunération . brut de base: 10000 contrat salarié . régime des impatriés: oui - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . aides employeur . emploi franc . éligible: oui - - 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 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . temps de travail . temps partiel: oui - - contrat salarié . rémunération . brut de base . équivalent temps plein: 2500€/mois + - contrat salarié . rémunération . brut de base . équivalent temps plein: 2500 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 €/mois + - contrat salarié . rémunération . brut de base: 1000 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 €/mois + - contrat salarié . rémunération . brut de base: 2300 contrat salarié . rémunération . primes . fin d'année . treizième mois: oui - - contrat salarié . rémunération . brut de base: 2300 €/mois + - contrat salarié . rémunération . brut de base: 2300 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 €/mois + - contrat salarié . rémunération . brut de base: 2300 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 €/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é . 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é . CDD . durée contrat: 2 - 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 . 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 . taux personnalisé: 10 heures supplémentaires et complémentaires: - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . temps de travail . heures supplémentaires: 5 - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . temps de travail . heures supplémentaires: 30 - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . temps de travail . heures supplémentaires: 5 entreprise . effectif: 100 - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . temps de travail . heures supplémentaires: 5 - contrat salarié . convention collective: "'HCR'" - - contrat salarié . rémunération . brut de base: 2000 €/mois + contrat salarié . convention collective: 'HCR' + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . temps de travail . heures supplémentaires: 30 - contrat salarié . convention collective: "'HCR'" - - contrat salarié . rémunération . brut de base: 2000 €/mois + contrat salarié . convention collective: 'HCR' + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . temps de travail . heures supplémentaires: 30 - contrat salarié . convention collective: "'compta'" - - contrat salarié . rémunération . brut de base: 2000 €/mois + contrat salarié . convention collective: 'compta' + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 3000 contrat salarié . statut JEI: oui - - contrat salarié . rémunération . brut de base: 20000 €/mois + - contrat salarié . rémunération . brut de base: 20000 contrat salarié . statut JEI: oui - - contrat salarié . rémunération . brut de base: 4000 €/mois - dirigeant: "'assimilé salarié'" + - contrat salarié . rémunération . brut de base: 4000 + dirigeant: 'assimilé salarié' contrat salarié . statut JEI: oui frais pro - titres restaurant: - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 3000 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 3200 contrat salarié . frais professionnels . indemnité kilométrique vélo: oui - - contrat salarié . rémunération . brut de base: 3200 €/mois + - contrat salarié . rémunération . brut de base: 3200 contrat salarié . frais professionnels . indemnité kilométrique vélo . distance mensuelle: 200 - - contrat salarié . rémunération . net après impôt: 1630 €/mois + - contrat salarié . rémunération . net après impôt: 1630 contrat salarié . frais professionnels . indemnité kilométrique vélo . distance mensuelle: 30 frais pro - DFS: - - 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é . 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é . 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 €/mois - contrat salarié . profession spécifique: "'journaliste'" - - contrat salarié . rémunération . brut de base: 2600 €/mois - contrat salarié . profession spécifique: "'journaliste'" + - 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 activité partielle: - - contrat salarié . rémunération . brut de base: 1560 €/mois + - contrat salarié . rémunération . brut de base: 1560 contrat salarié . activité partielle: oui - - contrat salarié . rémunération . brut de base: 4000 €/mois + - contrat salarié . rémunération . brut de base: 4000 contrat salarié . activité partielle: oui - - contrat salarié . rémunération . brut de base: 8000 €/mois + - contrat salarié . rémunération . brut de base: 8000 contrat salarié . activité partielle: oui - - contrat salarié . rémunération . brut de base: 4000 €/mois + - contrat salarié . rémunération . brut de base: 4000 contrat salarié . activité partielle: oui contrat salarié . activité partielle . heures travaillées: 30.33331 - - contrat salarié . rémunération . brut de base: 4000 €/mois + - contrat salarié . rémunération . brut de base: 4000 contrat salarié . activité partielle: oui contrat salarié . activité partielle . heures travaillées: 75.833275 - - contrat salarié . rémunération . brut de base: 3000 €/mois + - contrat salarié . rémunération . brut de base: 3000 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 €/mois + - contrat salarié . rémunération . brut de base: 4000 contrat salarié . activité partielle: oui - 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 contrat salarié . activité partielle: oui contrat salarié . activité partielle . convention syntec: oui - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 6000 contrat salarié . activité partielle: oui contrat salarié . activité partielle . convention syntec: oui lodeom: - - contrat salarié . rémunération . brut de base: 1521.22 €/mois + - contrat salarié . rémunération . brut de base: 1521.22 contrat salarié . lodeom . zone un: oui - - contrat salarié . rémunération . brut de base: 2000 €/mois + - contrat salarié . rémunération . brut de base: 2000 contrat salarié . lodeom . zone un: oui - - contrat salarié . rémunération . brut de base: 3000 €/mois + - contrat salarié . rémunération . brut de base: 3000 contrat salarié . lodeom . zone un: oui - - contrat salarié . rémunération . brut de base: 4000 €/mois + - contrat salarié . rémunération . brut de base: 4000 contrat salarié . lodeom . zone un: oui - - contrat salarié . rémunération . brut de base: 5500 €/mois + - contrat salarié . rémunération . brut de base: 5500 contrat salarié . lodeom . zone un: oui lodeom compétitivité renforcée: - - contrat salarié . rémunération . brut de base: 1521.22 €/mois + - contrat salarié . rémunération . brut de base: 1521.22 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 3000 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 €/mois + - contrat salarié . rémunération . brut de base: 4000 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 €/mois + - contrat salarié . rémunération . brut de base: 5500 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 €/mois + - contrat salarié . rémunération . brut de base: 1521.22 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 €/mois + - contrat salarié . rémunération . brut de base: 2000 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 €/mois + - contrat salarié . rémunération . brut de base: 3000 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 €/mois + - contrat salarié . rémunération . brut de base: 4000 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 €/mois + - contrat salarié . rémunération . brut de base: 5500 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 €/mois + - contrat salarié . rémunération . brut de base: 1521.22 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 €/mois + - contrat salarié . rémunération . brut de base: 2500 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 €/mois + - contrat salarié . rémunération . brut de base: 1521.22 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 €/mois + - contrat salarié . rémunération . brut de base: 2500 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 7dbb82f73..638c47f0e 100644 --- a/test/regressions/simulations.jest.js +++ b/test/regressions/simulations.jest.js @@ -20,16 +20,25 @@ import remunerationDirigeantSituations from './simulations-rémunération-dirige import employeeSituations from './simulations-salarié.yaml' const roundResult = arr => arr.map(x => Math.round(x)) -const engine = new Engine(rules) -const runSimulations = (situations, targets, baseSituation = {}) => +const engine = new Engine({ rules }) +const runSimulations = ( + situations, + targets, + baseSituation = {}, + defaultUnits, + namePrefix = '' +) => Object.entries(situations).map(([name, situations]) => situations.forEach(situation => { engine.setSituation({ ...baseSituation, ...situation }) - const res = targets.map(target => engine.evaluate(target).nodeValue) + engine.setDefaultUnits(defaultUnits) + const res = engine.evaluate(targets).map(node => node.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(name) + expect(JSON.stringify(roundResult(res))).toMatchSnapshot( + namePrefix + ' ' + name + ) }) ) @@ -37,7 +46,8 @@ it('calculate simulations-salarié', () => { runSimulations( employeeSituations, employeeConfig.objectifs, - employeeConfig.situation + employeeConfig.situation, + ['€/mois'] ) }) @@ -46,57 +56,38 @@ it('calculate simulations-indépendant', () => { (acc, cur) => [...acc, ...cur.objectifs], [] ) - runSimulations(independentSituations, targets, independantConfig.situation) + runSimulations(independentSituations, targets, independantConfig.situation, [ + '€/an' + ]) }) it('calculate simulations-auto-entrepreneur', () => { runSimulations( autoEntrepreneurSituations, autoentrepreneurConfig.objectifs, - autoentrepreneurConfig.situation + autoentrepreneurConfig.situation, + ['€/an'] ) }) -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-rémunération-dirigeant', () => { + const baseSituation = remunerationDirigeantConfig.situation + remunerationDirigeantConfig.branches.forEach(({ nom, situation }) => { + runSimulations( + remunerationDirigeantSituations, + remunerationDirigeantConfig.objectifs, + { ...baseSituation, ...situation }, + ['€/an'], + `${nom} - ` + ) + }) }) it('calculate simulations-artiste-auteur', () => { runSimulations( artisteAuteurSituations, artisteAuteurConfig.objectifs, - artisteAuteurConfig.situation + artisteAuteurConfig.situation, + ['€/an'] ) }) diff --git a/test/rules/co2.yaml b/test/rules/co2.yaml index 5a4e93422..b0da5e3e4 100644 --- a/test/rules/co2.yaml +++ b/test/rules/co2.yaml @@ -8,7 +8,8 @@ douche . impact: douche . nombre: question: Combien prenez-vous de douches ? - par défaut: 30 douches + unité: douche + par défaut: 30 suggestions: Une par jour: 30 @@ -53,7 +54,7 @@ chauffage . type: - gaz - fioul - électricité - par défaut: "'gaz'" + par défaut: gaz chauffage . type . gaz: icônes: 🔵 @@ -101,7 +102,8 @@ chauffage . impact par litre: douche . durée de la douche: question: Combien de temps dure votre douche en général ? - par défaut: 5 min + unité: min + par défaut: 5 suggestions: expresse: 5 moyenne: 10 diff --git a/test/rules/sasu.yaml b/test/rules/sasu.yaml index 86bb8ecce..55de08d28 100644 --- a/test/rules/sasu.yaml +++ b/test/rules/sasu.yaml @@ -3,13 +3,16 @@ # dans la base centrale chiffre affaires: - par défaut: 0 €/mois + unité par défaut: €/mois + par défaut: 0 charges: - par défaut: 0 €/mois + unité: €/mois + par défaut: 0 répartition salaire sur dividendes: - par défaut: 50% + par défaut: 50 + unité: '%' impôt sur les sociétés: formule: diff --git a/test/tree.test.js b/test/tree.test.js new file mode 100644 index 000000000..b81c9fd3a --- /dev/null +++ b/test/tree.test.js @@ -0,0 +1,439 @@ +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 new file mode 100644 index 000000000..e69de29bb diff --git a/test/variables.test.js b/test/variables.test.js index 6ea35c146..3d1477f72 100644 --- a/test/variables.test.js +++ b/test/variables.test.js @@ -10,6 +10,22 @@ 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' },