From 8abc5b7fcb82dad499c18ad04db578d9d97e2500 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Fri, 17 Jan 2020 15:05:54 +0100 Subject: [PATCH 01/13] =?UTF-8?q?:bug:=20r=C3=A9pare=20le=20changement=20d?= =?UTF-8?q?e=20p=C3=A9riode=20pour=20les=20charges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/mon-entreprise/simulateurs.js | 10 ++++- source/reducers/rootReducer.ts | 39 +------------------ 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/cypress/integration/mon-entreprise/simulateurs.js b/cypress/integration/mon-entreprise/simulateurs.js index 99b1e9334..5bd07caf1 100644 --- a/cypress/integration/mon-entreprise/simulateurs.js +++ b/cypress/integration/mon-entreprise/simulateurs.js @@ -1,5 +1,6 @@ const fr = Cypress.env('language') === 'fr' const inputSelector = 'input.currencyInput__input:not([name$="charges"])' +const chargeInputSelector = 'input.currencyInput__input[name$="charges"]' describe('Simulateurs', function() { if (!fr) { return @@ -15,7 +16,7 @@ describe('Simulateurs', function() { it('should display a result when entering a value in any of the currency input', () => { cy.contains('€ / an').click() if (['indépendant', 'assimilé-salarié'].includes(simulateur)) { - cy.get('input.currencyInput__input[name$="charges"]').type(1000) + cy.get(chargeInputSelector).type(1000) } cy.get(inputSelector).each((testedInput, i) => { cy.wrap(testedInput).type('{selectall}60000') @@ -37,12 +38,19 @@ describe('Simulateurs', function() { cy.get(inputSelector) .first() .type('{selectall}12000') + if (['indépendant', 'assimilé-salarié'].includes(simulateur)) { + cy.get(chargeInputSelector).type('{selectall}6000') + } cy.wait(600) cy.contains('€ / mois').click() cy.get(inputSelector) .first() .invoke('val') .should('match', /1[\s]000/) + cy.get(chargeInputSelector) + .first() + .invoke('val') + .should('be', '500') }) it('should allow to navigate to a documentation page', function() { diff --git a/source/reducers/rootReducer.ts b/source/reducers/rootReducer.ts index c496e64f6..ef20b6cc4 100644 --- a/source/reducers/rootReducer.ts +++ b/source/reducers/rootReducer.ts @@ -1,6 +1,6 @@ import { Action } from 'Actions/actions' import { Analysis } from 'Engine/traverse' -import { areUnitConvertible, convertUnit, parseUnit, Unit } from 'Engine/units' +import { Unit } from 'Engine/units' import { defaultTo, identity, omit, without } from 'ramda' import reduceReducers from 'reduce-reducers' import { combineReducers, Reducer } from 'redux' @@ -110,37 +110,6 @@ function updateSituation( return { ...removePreviousTarget(situation), [fieldName]: value } } -function updateDefaultUnit(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' @@ -251,11 +220,7 @@ function simulation( case 'UPDATE_DEFAULT_UNIT': return { ...state, - defaultUnits: [action.defaultUnit], - situation: updateDefaultUnit(state.situation, { - toUnit: action.defaultUnit, - analysis - }) + defaultUnits: [action.defaultUnit] } } return state From 8ca9f82a178d28fed1a77c2db7ec5f6d03f38606 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Fri, 17 Jan 2020 17:06:27 +0100 Subject: [PATCH 02/13] =?UTF-8?q?:hammer:=20proposition=20pour=20le=20m?= =?UTF-8?q?=C3=A9canisme=20de=20variable=20temporelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/components/PaySlip.tsx | 4 +- source/components/PaySlipSections.js | 11 ++- source/components/PeriodSwitch.tsx | 5 +- source/components/SalaryExplanation.tsx | 6 +- source/components/Value.tsx | 4 + source/components/conversation/DateInput.tsx | 1 + .../simulationConfigs/artiste-auteur.yaml | 2 +- .../simulationConfigs/assimilé.yaml | 2 +- .../simulationConfigs/auto-entrepreneur.yaml | 2 +- .../simulationConfigs/indépendant.yaml | 2 +- .../rémunération-dirigeant.yaml | 2 +- .../components/simulationConfigs/salarié.yaml | 2 +- source/components/ui/AnimatedTargetValue.tsx | 2 +- source/reducers/rootReducer.ts | 10 +-- source/selectors/analyseSelectors.ts | 4 +- test/mécanismes/période.yaml | 76 +++++++++++++++++++ 16 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 test/mécanismes/période.yaml diff --git a/source/components/PaySlip.tsx b/source/components/PaySlip.tsx index 973557258..7eda9358d 100644 --- a/source/components/PaySlip.tsx +++ b/source/components/PaySlip.tsx @@ -103,13 +103,13 @@ export default function PaySlip() { {/* Salaire chargé */} diff --git a/source/components/PaySlipSections.js b/source/components/PaySlipSections.js index 3b8659807..0c72d71fd 100644 --- a/source/components/PaySlipSections.js +++ b/source/components/PaySlipSections.js @@ -1,6 +1,8 @@ import Value from 'Components/Value' import React from 'react' import { Trans } from 'react-i18next' +import { useSelector } from 'react-redux' +import { defaultUnitSelector } from 'Selectors/analyseSelectors' import RuleLink from './RuleLink' export let SalaireBrutSection = ({ getRule }) => { @@ -43,12 +45,13 @@ export let SalaireBrutSection = ({ getRule }) => { ) } -export let Line = ({ rule, ...props }) => ( - <> +export let Line = ({ rule, ...props }) => { + const defaultUnit = useSelector(defaultUnitSelector) + ;<> - + -) +} export let SalaireNetSection = ({ getRule }) => { let avantagesEnNature = getRule( diff --git a/source/components/PeriodSwitch.tsx b/source/components/PeriodSwitch.tsx index dbc8fd0d4..a42b7be43 100644 --- a/source/components/PeriodSwitch.tsx +++ b/source/components/PeriodSwitch.tsx @@ -3,13 +3,14 @@ import { parseUnit, serializeUnit } from 'Engine/units' import React from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { defaultUnitsSelector } from 'Selectors/analyseSelectors' +import { defaultUnitSelector } from 'Selectors/analyseSelectors' import './PeriodSwitch.css' export default function PeriodSwitch() { const dispatch = useDispatch() - const currentUnit = useSelector(defaultUnitsSelector)[0] const language = useTranslation().i18n.language + const currentUnit = useSelector(defaultUnitSelector) + let units = ['€/mois', '€/an'] return ( diff --git a/source/components/SalaryExplanation.tsx b/source/components/SalaryExplanation.tsx index 20d50f780..b4c8b3cd9 100644 --- a/source/components/SalaryExplanation.tsx +++ b/source/components/SalaryExplanation.tsx @@ -10,7 +10,7 @@ import { useSelector } from 'react-redux' import { RootState } from 'Reducers/rootReducer' import { analysisWithDefaultsSelector, - defaultUnitsSelector + defaultUnitSelector } from 'Selectors/analyseSelectors' import * as Animate from 'Ui/animate' @@ -51,9 +51,9 @@ export default function SalaryExplanation() { {emoji('📊')} Voir la répartition des cotisations -
+
)} @@ -117,7 +117,7 @@ function RevenueRepatitionSection() { } function PaySlipSection() { - const unit = useSelector(defaultUnitsSelector)[0] + const unit = useSelector(defaultUnitSelector) return (

diff --git a/source/components/Value.tsx b/source/components/Value.tsx index 5d02570b7..464b7ca44 100644 --- a/source/components/Value.tsx +++ b/source/components/Value.tsx @@ -26,12 +26,16 @@ export type ValueProps = Partial< children: number negative: boolean customCSS: string + defaultUnit?: string + printedUnit?: string } > export default function Value({ nodeValue: value, unit, + defaultUnit, + printedUnit, nilValueSymbol, maximumFractionDigits, minimumFractionDigits, diff --git a/source/components/conversation/DateInput.tsx b/source/components/conversation/DateInput.tsx index aa1f337d9..c4322772f 100644 --- a/source/components/conversation/DateInput.tsx +++ b/source/components/conversation/DateInput.tsx @@ -13,6 +13,7 @@ export default function DateInput({ suggestions, onChange, onSubmit, value }) { const handleDateChange = useCallback( evt => { + console.log('target', evt.target) if (!evt.target.value) { return onChange(null) } diff --git a/source/components/simulationConfigs/artiste-auteur.yaml b/source/components/simulationConfigs/artiste-auteur.yaml index a9573a517..c9d5494df 100644 --- a/source/components/simulationConfigs/artiste-auteur.yaml +++ b/source/components/simulationConfigs/artiste-auteur.yaml @@ -1,5 +1,5 @@ situation: dirigeant: artiste-auteur -unités par défaut: [€/an] +unité par défaut: €/an objectifs: - artiste-auteur . cotisations diff --git a/source/components/simulationConfigs/assimilé.yaml b/source/components/simulationConfigs/assimilé.yaml index 758a2d41c..c7ea9640b 100644 --- a/source/components/simulationConfigs/assimilé.yaml +++ b/source/components/simulationConfigs/assimilé.yaml @@ -34,7 +34,7 @@ questions: - contrat salarié . complémentaire santé . part employeur - contrat salarié . régime des impatriés -unités par défaut: [€/an] +unité par défaut: €/an situation: 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 d98b090dd..3ed92ae1f 100644 --- a/source/components/simulationConfigs/auto-entrepreneur.yaml +++ b/source/components/simulationConfigs/auto-entrepreneur.yaml @@ -16,6 +16,6 @@ questions: liste noire: - entreprise . charges -unités par défaut: [€/an] +unité par défaut: €/an situation: dirigeant: 'auto-entrepreneur' diff --git a/source/components/simulationConfigs/indépendant.yaml b/source/components/simulationConfigs/indépendant.yaml index 13e230a36..a167b3549 100644 --- a/source/components/simulationConfigs/indépendant.yaml +++ b/source/components/simulationConfigs/indépendant.yaml @@ -29,6 +29,6 @@ questions: - dirigeant . indépendant . cotisations et contributions . exonérations . âge - dirigeant . indépendant . cotisations et contributions . exonérations . invalidité -unités par défaut: [€/an] +unité par défaut: €/an situation: dirigeant: 'indépendant' diff --git a/source/components/simulationConfigs/rémunération-dirigeant.yaml b/source/components/simulationConfigs/rémunération-dirigeant.yaml index 327a200a3..512437a8b 100644 --- a/source/components/simulationConfigs/rémunération-dirigeant.yaml +++ b/source/components/simulationConfigs/rémunération-dirigeant.yaml @@ -18,7 +18,7 @@ questions: - entreprise . catégorie d'activité . restauration ou hébergement - entreprise . catégorie d'activité . libérale règlementée -unités par défaut: [€/an] +unité par défaut: €/an branches: - nom: Assimilé salarié situation: diff --git a/source/components/simulationConfigs/salarié.yaml b/source/components/simulationConfigs/salarié.yaml index dc159f44e..93899f0c8 100644 --- a/source/components/simulationConfigs/salarié.yaml +++ b/source/components/simulationConfigs/salarié.yaml @@ -27,6 +27,6 @@ questions: - contrat salarié . statut JEI - contrat salarié . complémentaire santé . part employeur - contrat salarié . régime des impatriés -unités par défaut: [€/mois] +unité par défaut: €/mois situation: dirigeant: non diff --git a/source/components/ui/AnimatedTargetValue.tsx b/source/components/ui/AnimatedTargetValue.tsx index 2d86bc464..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?.defaultUnits[0] + (state: RootState) => state?.simulation?.defaultUnit ) const previousUnit = useRef(currentUnit) diff --git a/source/reducers/rootReducer.ts b/source/reducers/rootReducer.ts index ef20b6cc4..07fa0aaa1 100644 --- a/source/reducers/rootReducer.ts +++ b/source/reducers/rootReducer.ts @@ -37,7 +37,7 @@ type Example = null | { name: string situation: object dottedName: DottedName - defaultUnits?: Array + defaultUnit?: Unit } function currentExample(state: Example = null, action: Action): Example { @@ -124,7 +124,7 @@ export type SimulationConfig = Partial<{ bloquant: Array situation: Simulation['situation'] branches: Array<{ nom: string; situation: SimulationConfig['situation'] }> - 'unités par défaut': [string] + 'unité par défaut': string }> type Situation = Partial> @@ -134,7 +134,7 @@ export type Simulation = { hiddenControls: Array situation: Situation initialSituation: Situation - defaultUnits: [string] + defaultUnit: string foldedSteps: Array unfoldedStep?: DottedName | null } @@ -172,7 +172,7 @@ function simulation( hiddenControls: [], situation: companySituation, initialSituation: companySituation, - defaultUnits: config['unités par défaut'] || ['€/mois'], + defaultUnit: config['unité par défaut'] || '€/mois', foldedSteps: Object.keys(companySituation) as Array, unfoldedStep: null } @@ -220,7 +220,7 @@ function simulation( case 'UPDATE_DEFAULT_UNIT': return { ...state, - defaultUnits: [action.defaultUnit] + defaultUnit: action.defaultUnit } } return state diff --git a/source/selectors/analyseSelectors.ts b/source/selectors/analyseSelectors.ts index e6579d3db..ffacc073e 100644 --- a/source/selectors/analyseSelectors.ts +++ b/source/selectors/analyseSelectors.ts @@ -110,8 +110,8 @@ let validatedStepsSelector = createSelector( [state => state.simulation?.foldedSteps, targetNamesSelector], (foldedSteps, targetNames) => [...(foldedSteps || []), ...targetNames] ) -export const defaultUnitsSelector = (state: RootState) => - state.simulation?.defaultUnits || [] +export const defaultUnitSelector = (state: RootState) => + state.simulation?.defaultUnit let branchesSelector = (state: RootState) => configSelector(state).branches let configSituationSelector = (state: RootState) => configSelector(state).situation || {} diff --git a/test/mécanismes/période.yaml b/test/mécanismes/période.yaml new file mode 100644 index 000000000..192642138 --- /dev/null +++ b/test/mécanismes/période.yaml @@ -0,0 +1,76 @@ +contrat salarié . date d'embauche: + type: date + formule: 25/04/2018 + +contrat salarié . salaire . brut de base: + formule: + calendrier: + - depuis: date d'embauche + montant: 2000€/mois + - depuis: 09/08/2019 + montant: 2200€/mois + +salaire . primes: + formule: + calendrier: + - en: décembre 2019 + montant: 2000€/mois + +salaire . brut: + formules: + - brut de base + - primes + +test . calcul brut et prime: + formule: salaire brut [décembre 2019] + exemples: + - valeur attendue: 4200 # 2000 + 2200 + +test . proratisation du salaire avec entrée en cours de mois: + formule: salaire brut [avril 2019] + exemples: + - valeur attendue: 400 # (2000 * 6)/30 + +test . proratisation du salaire avec augmentation en cours de mois: + formule: salaire brut [juillet 2019] + exemples: + - valeur attendue: 2148.39 # (2000 * 8 + 2200 * 23)/31 + +PSS: + formule: + calendrier: + - en: 2019 + montant: 3377 €/mois + - en: 2020 + montant: 3428 €/mois + +contrat salarié . PSS proratisé: + période: + depuis: date d'embauche + formule: PSS + +test . proratisation du PSS pour le mois d'embauche: + période: + en: avril 2019 + formule: PSS proratisé + exemples: + - valeur attendue: 2183 + +cotisations . retraite employeur plafonnée: + formule: + multiplication: + taux: 8.55% + assiette: salaire brut + plafond: PSS proratisé + régularisation: progressive sur l'année civile + +test . régularisation: + description: >- + Bien que le salaire du mois de décembre soit supérieur au + plafond, le taux effectif reste 8.55% du fait de la + régularisation des mois précédents + période: + en: décembre 2019 + formule: retraite employeur plafonnée + exemples: + - valeur attendue: 359.1 #4200 * 8.55% From 7b18252798575ba8d0ebe4a29096b67966ee510a Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Tue, 4 Feb 2020 18:33:03 +0100 Subject: [PATCH 03/13] =?UTF-8?q?:hammer:=20Premi=C3=A8re=20impl=C3=A9ment?= =?UTF-8?q?ation=20des=20variable=20temporelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Uniquement pour les valeur numérique - Pour les cas simple applicable / non applicable (pas de cas mixte) - Pas d'implémentation de mécanisme (addition / barème / etc) --- publicode/rules/dirigeant.yaml | 1 - source/engine/evaluateRule.ts | 1 + source/engine/grammar.ne | 35 +++- source/engine/grammarFunctions.js | 8 + .../engine/mecanisms/variableTemporelle.tsx | 52 ++++++ source/engine/parse.tsx | 2 + source/engine/parseReference.js | 6 +- source/engine/parseRule.tsx | 12 +- source/engine/period.tsx | 91 ++++++++++ test/mécanismes/période.yaml | 76 -------- test/mécanismes/variable-temporelle.yaml | 169 ++++++++++++++++++ 11 files changed, 363 insertions(+), 90 deletions(-) create mode 100644 source/engine/mecanisms/variableTemporelle.tsx create mode 100644 source/engine/period.tsx delete mode 100644 test/mécanismes/période.yaml create mode 100644 test/mécanismes/variable-temporelle.yaml diff --git a/publicode/rules/dirigeant.yaml b/publicode/rules/dirigeant.yaml index 95eb2ee99..ebe82861c 100644 --- a/publicode/rules/dirigeant.yaml +++ b/publicode/rules/dirigeant.yaml @@ -1032,7 +1032,6 @@ dirigeant . indépendant . cotisations et contributions . cotisations . allocati taux: 3.1% dirigeant . indépendant . cotisations et contributions . exonérations: - période: flexible formule: somme: - ZFU diff --git a/source/engine/evaluateRule.ts b/source/engine/evaluateRule.ts index a4b4b6a7b..daa9db1ce 100644 --- a/source/engine/evaluateRule.ts +++ b/source/engine/evaluateRule.ts @@ -121,6 +121,7 @@ export default (cache, situationGate, parsedRules, node) => { ...(node.formule && { formule: evaluatedFormula }), nodeValue, unit, + period, isApplicable, missingVariables } diff --git a/source/engine/grammar.ne b/source/engine/grammar.ne index 9cc736d1b..7d017d2f9 100644 --- a/source/engine/grammar.ne +++ b/source/engine/grammar.ne @@ -6,25 +6,33 @@ # @preprocessor esmodule @{% -const {string, filteredVariable, date, variable, variableWithConversion, binaryOperation, unaryOperation, boolean, number, numberWithUnit } = require('./grammarFunctions') +const { + string, filteredVariable, date, variable, variableWithConversion, + temporalNumericValue, binaryOperation, unaryOperation, boolean, number, + numberWithUnit +} = require('./grammarFunctions') const moo = require("moo"); -const dateRegexp = /(?:(?:0?[1-9]|[12][0-9]|3[01])\/)?(?:0?[1-9]|1[012])\/\d{4}/ +const dateRegexp = `(?:(?:0?[1-9]|[12][0-9]|3[01])\\/)?(?:0?[1-9]|1[012])\\/\\d{4}` const letter = '[a-zA-Z\u00C0-\u017F€$%]'; const letterOrNumber = '[a-zA-Z\u00C0-\u017F0-9\']'; -const word = `${letter}(?:[\-']?${letterOrNumber}+)*`; +const word = `${letter}(?:[-']?${letterOrNumber}+)*`; const wordOrNumber = `(?:${word}|${letterOrNumber}+)` const words = `${word}(?:[\\s]?${wordOrNumber}+)*` +const periodWord = `\\| ${word}(?:[\\s]${word})*` + const numberRegExp = '-?(?:[1-9][0-9]+|[0-9])(?:\\.[0-9]+)?'; const lexer = moo.compile({ - date: dateRegexp, '(': '(', ')': ')', '[': '[', ']': ']', comparison: ['>','<','>=','<=','=','!='], infinity: 'Infinity', + colon: " : ", + date: new RegExp(dateRegexp), + periodWord: new RegExp(periodWord), words: new RegExp(words), number: new RegExp(numberRegExp), string: /'[ \t\.'a-zA-Z\-\u00C0-\u017F0-9 ]+'/, @@ -33,7 +41,7 @@ const lexer = moo.compile({ dot: ' . ', '.': '.', letterOrNumber: new RegExp(letterOrNumber), - space: { match: /[\s]+/, lineBreaks: true } + space: { match: /[\s]+/, lineBreaks: true }, }); const join = (args) => ({value: (args.map(x => x && x.value).join(""))}) @@ -43,12 +51,20 @@ const flattenJoin = ([a, b]) => Array.isArray(b) ? join([a, ...b]) : a @lexer lexer main -> - AdditionSubstraction {% id %} - | Comparison {% id %} - | NonNumericTerminal {% id %} - | Negation {% id %} + Comparison {% id %} + | NumericValue {% id %} | Date {% id %} + | NonNumericTerminal {% id %} +NumericValue -> + AdditionSubstraction {% id %} + | Negation {% id %} + | TemporalNumericValue {% id %} + +TemporalNumericValue -> + NumericValue %space %periodWord %space %date {% ([value,,word,,dateString]) => temporalNumericValue(value, word, date([dateString])) %} + | NumericValue %space %periodWord %colon Date {% ([value,,word,,date]) => temporalNumericValue(value, word, date) %} + NumericTerminal -> Variable {% id %} | VariableWithUnitConversion {% id %} @@ -92,6 +108,7 @@ VariableWithUnitConversion -> Filter -> "." %words {% ([,filter]) => filter %} FilteredVariable -> Variable %space Filter {% filteredVariable %} + AdditionSubstraction -> AdditionSubstraction %space %additionSubstraction %space MultiplicationDivision {% binaryOperation('calculation') %} | MultiplicationDivision {% id %} diff --git a/source/engine/grammarFunctions.js b/source/engine/grammarFunctions.js index eecf5c3ad..0ba8986ad 100644 --- a/source/engine/grammarFunctions.js +++ b/source/engine/grammarFunctions.js @@ -2,6 +2,7 @@ The advantage of putting them here is to get prettier's JS formatting, since Nealrey doesn't support it https://github.com/kach/nearley/issues/310 */ import { normalizeDateString } from 'Engine/date' import { parseUnit } from 'Engine/units' +import { parsePeriod } from './period' export let binaryOperation = operationType => ([A, , operator, , B]) => ({ [operator]: { @@ -25,6 +26,13 @@ export let variableWithConversion = ([{ variable }, , unit]) => ({ unitConversion: { explanation: variable, unit: parseUnit(unit.value) } }) +export let temporalNumericValue = (variable, word, date) => ({ + temporalValue: { + explanation: variable, + period: parsePeriod(word.value.slice(2), date) + } +}) + export let variable = ([firstFragment, nextFragment], _, reject) => { const fragments = [firstFragment, ...nextFragment].map(({ value }) => value) if (!nextFragment.length && ['oui', 'non'].includes(firstFragment)) { diff --git a/source/engine/mecanisms/variableTemporelle.tsx b/source/engine/mecanisms/variableTemporelle.tsx new file mode 100644 index 000000000..f155406ea --- /dev/null +++ b/source/engine/mecanisms/variableTemporelle.tsx @@ -0,0 +1,52 @@ +import { bonus, evaluateNode, mergeMissing } from 'Engine/evaluation' +import { isValidPeriod, periodIntersection } from 'Engine/period' + +function evaluate( + cache: any, + situation: any, + parsedRules: any, + node: ReturnType +) { + const evaluateAttribute = evaluateNode.bind( + null, + cache, + situation, + parsedRules + ) + const start = node.period.start && evaluateAttribute(node.period.start) + const end = node.period.end && evaluateAttribute(node.period.end) + const explanation = evaluateAttribute(node.explanation) + const period = periodIntersection(explanation.period, { start, end }) + let nodeValue: any = null + let missingVariables = mergeMissing( + period.start?.missingVariables, + period.end?.missingVariables + ) + if (isValidPeriod(period)) { + nodeValue = explanation.nodeValue + missingVariables = mergeMissing( + bonus(missingVariables, true), + explanation.missingVariables + ) + } else { + nodeValue = false + } + return { + ...node, + nodeValue, + period, + explanation, + missingVariables + } +} + +export default function parseVariableTemporelle(parse, __, v: any) { + return { + evaluate, + explanation: parse(v.explanation), + period: { + start: v.period.start && parse(v.period.start), + end: v.period.end && parse(v.period.end) + } + } +} diff --git a/source/engine/parse.tsx b/source/engine/parse.tsx index 1a7734f0f..c84c73885 100644 --- a/source/engine/parse.tsx +++ b/source/engine/parse.tsx @@ -10,6 +10,7 @@ import encadrement from 'Engine/mecanisms/encadrement' import grille from 'Engine/mecanisms/grille' import operation from 'Engine/mecanisms/operation' import tauxProgressif from 'Engine/mecanisms/tauxProgressif' +import variableTemporelle from 'Engine/mecanisms/variableTemporelle' import variations from 'Engine/mecanisms/variations' import { Grammar, Parser } from 'nearley' import { @@ -166,6 +167,7 @@ const statelessParseFunction = { somme: mecanismSum, multiplication: mecanismProduct, produit: mecanismProduct, + temporalValue: variableTemporelle, arrondi: mecanismRound, barème, grille, diff --git a/source/engine/parseReference.js b/source/engine/parseReference.js index aad038595..df4fa7f2f 100644 --- a/source/engine/parseReference.js +++ b/source/engine/parseReference.js @@ -149,10 +149,11 @@ 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, period) => { cache[cacheName] = { ...node, nodeValue, + period, ...(explanation && { explanation }), @@ -183,7 +184,8 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v return cacheNode( evaluation.nodeValue, evaluation.missingVariables, - evaluation + evaluation, + evaluation.period ) } diff --git a/source/engine/parseRule.tsx b/source/engine/parseRule.tsx index 67dbcbd90..aa22d1f54 100644 --- a/source/engine/parseRule.tsx +++ b/source/engine/parseRule.tsx @@ -83,8 +83,15 @@ export default (rules, rule, parsedRules) => { parsedRules, node.explanation ), - { nodeValue, unit, missingVariables } = explanation - return { ...node, nodeValue, unit, missingVariables, explanation } + { nodeValue, unit, missingVariables, period } = explanation + return { + ...node, + nodeValue, + unit, + missingVariables, + explanation, + period + } } let child = parse(rules, rule, parsedRules)(value) @@ -147,6 +154,7 @@ export default (rules, rule, parsedRules) => { missingVariables: mergeAllMissing(isDisabledBy) } }, + jsx: (_nodeValue, { isDisabledBy }) => { return ( isDisabledBy.length > 0 && ( diff --git a/source/engine/period.tsx b/source/engine/period.tsx new file mode 100644 index 000000000..09c260563 --- /dev/null +++ b/source/engine/period.tsx @@ -0,0 +1,91 @@ +import { convertToDate } from 'Engine/date' + +export type Period = { + start: Date | null + end: Date | null +} + +export function parsePeriod(word: string, date: Date): Period { + const startWords = [ + 'depuis', + 'depuis le', + 'depuis la', + 'à partir de', + 'à partir du', + 'du' + ] + const endWords = [ + "jusqu'à", + "jusqu'au", + "jusqu'à la", + 'avant', + 'avant le', + 'avant la', + 'au' + ] + const intervalWords = ['le'] + if (!startWords.concat(endWords, intervalWords).includes(word)) { + throw new SyntaxError( + `Le mot clé '${word}' n'est pas valide. Les mots clés possible sont les suivants :\n\t ${startWords.join( + ', ' + )}` + ) + } + if (word === 'le') { + return { + start: date, + end: date + } + } + if (startWords.includes(word)) { + return { + start: date, + end: null + } + } + if (endWords.includes(word)) { + return { + start: null, + end: date + } + } + throw new Error('Non implémenté') +} + +export function periodIntersection( + parentPeriod: Period<{ nodeValue: string }> | null, + childPeriod: Period<{ nodeValue: string }> | null +): Period<{ nodeValue: string }> { + if (!parentPeriod) { + return childPeriod || { start: null, end: null } + } + if (!childPeriod) { + return parentPeriod || { start: null, end: null } + } + const startDateParent = + parentPeriod.start?.nodeValue && convertToDate(parentPeriod.start.nodeValue) + const startDateChild = + childPeriod.start?.nodeValue && convertToDate(childPeriod.start.nodeValue) + const endDateParent = + parentPeriod.end?.nodeValue && convertToDate(parentPeriod.end.nodeValue) + const endDateChild = + childPeriod.end?.nodeValue && convertToDate(childPeriod.end.nodeValue) + return { + start: + startDateChild == null || startDateParent > startDateChild + ? parentPeriod.start + : childPeriod.start, + end: + endDateParent == null || endDateParent > endDateChild + ? childPeriod.end + : parentPeriod.end + } +} + +export function isValidPeriod(period: Period<{ nodeValue: string }>): boolean { + return ( + period.start?.nodeValue == null || + period.end?.nodeValue == null || + convertToDate(period.end.nodeValue) >= convertToDate(period.start.nodeValue) + ) +} diff --git a/test/mécanismes/période.yaml b/test/mécanismes/période.yaml deleted file mode 100644 index 192642138..000000000 --- a/test/mécanismes/période.yaml +++ /dev/null @@ -1,76 +0,0 @@ -contrat salarié . date d'embauche: - type: date - formule: 25/04/2018 - -contrat salarié . salaire . brut de base: - formule: - calendrier: - - depuis: date d'embauche - montant: 2000€/mois - - depuis: 09/08/2019 - montant: 2200€/mois - -salaire . primes: - formule: - calendrier: - - en: décembre 2019 - montant: 2000€/mois - -salaire . brut: - formules: - - brut de base - - primes - -test . calcul brut et prime: - formule: salaire brut [décembre 2019] - exemples: - - valeur attendue: 4200 # 2000 + 2200 - -test . proratisation du salaire avec entrée en cours de mois: - formule: salaire brut [avril 2019] - exemples: - - valeur attendue: 400 # (2000 * 6)/30 - -test . proratisation du salaire avec augmentation en cours de mois: - formule: salaire brut [juillet 2019] - exemples: - - valeur attendue: 2148.39 # (2000 * 8 + 2200 * 23)/31 - -PSS: - formule: - calendrier: - - en: 2019 - montant: 3377 €/mois - - en: 2020 - montant: 3428 €/mois - -contrat salarié . PSS proratisé: - période: - depuis: date d'embauche - formule: PSS - -test . proratisation du PSS pour le mois d'embauche: - période: - en: avril 2019 - formule: PSS proratisé - exemples: - - valeur attendue: 2183 - -cotisations . retraite employeur plafonnée: - formule: - multiplication: - taux: 8.55% - assiette: salaire brut - plafond: PSS proratisé - régularisation: progressive sur l'année civile - -test . régularisation: - description: >- - Bien que le salaire du mois de décembre soit supérieur au - plafond, le taux effectif reste 8.55% du fait de la - régularisation des mois précédents - période: - en: décembre 2019 - formule: retraite employeur plafonnée - exemples: - - valeur attendue: 359.1 #4200 * 8.55% diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml new file mode 100644 index 000000000..de100cf24 --- /dev/null +++ b/test/mécanismes/variable-temporelle.yaml @@ -0,0 +1,169 @@ +variable temporelle numérique . le . valeur: + formule: 40 €/mois | le 02/04/2019 + +variable temporelle numérique . le . test date applicable: + formule: valeur | le 02/04/2019 + exemples: + - valeur attendue: 40 + +variable temporelle numérique . le . test date non applicable: + formule: valeur | le 02/03/2021 + exemples: + - valeur attendue: false + +variable temporelle numérique . depuis . valeur: + formule: 40 €/mois | depuis le 02/04/2019 + +variable temporelle numérique . depuis . test date applicable: + formule: valeur | depuis le 06/04/2019 + exemples: + - valeur attendue: 40 + +variable temporelle numérique . depuis . test date non applicable: + formule: valeur | le 08/03/2019 + exemples: + - valeur attendue: false + +variable temporelle numérique . intervalle . valeur: + formule: 40 €/mois | du 02/04/2019 | au 04/05/2020 + +variable temporelle numérique . intervalle . test date applicable: + formule: valeur | le 06/04/2019 + exemples: + - valeur attendue: 40 + +variable temporelle numérique . intervalle . test date applicable 2: + formule: valeur | depuis le 05/06/2019 | jusqu'au 19/04/2020 + exemples: + - valeur attendue: 40 + +variable temporelle numérique . intervalle . test date non applicable: + formule: valeur | le 08/03/2021 + exemples: + - valeur attendue: false + +variable temporelle numérique . intervalle . test date non applicable 2: + formule: valeur | le 28/01/2019 + exemples: + - valeur attendue: false + +variable temporelle numérique . date limite de paiement: + formule: 03/09/2020 + +variable temporelle numérique . majorations de retard: + formule: '40 €/jour | à partir de : date limite de paiement' + +variable temporelle numérique . test date non applicable: + formule: "majorations de retard | jusqu'au : 02/09/2020" + exemples: + - valeur attendue: false +# variable temporelle numérique . test date non applicable 2: +# formule: majorations de retard | du 01/02/2020 | au 03/08/2020 +# exemples: +# - valeur attendue: false + +# variable temporelle numérique . test date applicable: +# formule: 'majorations de retard | depuis la : date limite de paiement' +# exemples: +# - valeur attendue: 40 + +# variable temporelle numérique . test date applicable 2: +# formule: majorations de retard | le 03/09/2020 +# exemples: +# - valeur attendue: 40 +# variable temporelle numérique . test associativité: +# formule: valeur + 120 €/an | le 02/09/2019 +# exemples: +# - valeur attendue: 50 +# variable temporelle numérique . test date non applicable: +# formule: valeur +# exemples: +# - valeur attendue: non + +# variable temporelle numérique . test date applicable: +# formule: valeur [le 12/02/2020] +# exemples: +# - valeur attendue: 40 + +# variable temporelle numérique . test date applicable avec unité: +# formule: valeur [en 2019, en 2020, €/mois] +# unité: €/mois +# exemples: +# - valeur attendue: 40 + +# contrat salarié . date d'embauche: +# type: date +# formule: 25/04/2018 + +# contrat salarié . salaire . brut de base: +# formule: +# calendrier: +# - depuis: date d'embauche +# montant: 2000€/mois +# - depuis: 09/08/2019 +# montant: 2200€/mois + +# salaire . primes: +# formule: +# calendrier: +# - en: décembre 2019 +# montant: 2000€/mois + +# salaire . brut: +# formules: +# - brut de base +# - primes + +# test . calcul brut et prime: +# formule: salaire brut [décembre 2019] +# exemples: +# - valeur attendue: 4200 # 2000 + 2200 + +# test . proratisation du salaire avec entrée en cours de mois: +# formule: salaire brut [avril 2019] +# exemples: +# - valeur attendue: 400 # (2000 * 6)/30 + +# test . proratisation du salaire avec augmentation en cours de mois: +# formule: salaire brut [juillet 2019] +# exemples: +# - valeur attendue: 2148.39 # (2000 * 8 + 2200 * 23)/31 + +# PSS: +# formule: +# calendrier: +# - en: 2019 +# montant: 3377 €/mois +# - en: 2020 +# montant: 3428 €/mois + +# contrat salarié . PSS proratisé: +# période: +# depuis: date d'embauche +# formule: PSS + +# test . proratisation du PSS pour le mois d'embauche: +# période: +# en: avril 2019 +# formule: PSS proratisé +# exemples: +# - valeur attendue: 2183 + +# cotisations . retraite employeur plafonnée: +# formule: +# multiplication: +# taux: 8.55% +# assiette: salaire brut +# plafond: PSS proratisé +# régularisation: progressive sur l'année civile + +# test . régularisation: +# description: >- +# Bien que le salaire du mois de décembre soit supérieur au +# plafond, le taux effectif reste 8.55% du fait de la +# régularisation des mois précédents +# période: +# en: décembre 2019 +# formule: retraite employeur plafonnée +# exemples: +# - valeur attendue: 359.1 #4200 * 8.55% From 2526499ab7d5429f1d4b800bd9ef39db909eab10 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Sun, 16 Feb 2020 19:56:07 +0100 Subject: [PATCH 04/13] =?UTF-8?q?:gear:=20Ajoute=20le=20m=C3=A9canisme=20s?= =?UTF-8?q?omme=20pour=20les=20variables=20temporelles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- publicode/rules/dirigeant.yaml | 10 +- source/engine/date.ts | 10 + source/engine/error.ts | 30 +- source/engine/evaluateRule.ts | 3 +- .../engine/{evaluation.js => evaluation.tsx} | 38 ++- source/engine/grammar.ne | 3 +- source/engine/mecanisms.js | 28 +- source/engine/mecanisms/durée.tsx | 9 +- ...leTemporelle.tsx => variableTemporelle.ts} | 40 +-- source/engine/parse.tsx | 22 +- source/engine/parseReference.js | 6 +- source/engine/parseRule.tsx | 4 +- source/engine/period.ts | 258 ++++++++++++++++++ source/engine/period.tsx | 91 ------ source/engine/rules.js | 33 ++- test/mécanismes/variable-temporelle.yaml | 135 +++++---- test/period.test.js | 70 +++++ 17 files changed, 553 insertions(+), 237 deletions(-) rename source/engine/{evaluation.js => evaluation.tsx} (82%) rename source/engine/mecanisms/{variableTemporelle.tsx => variableTemporelle.ts} (52%) create mode 100644 source/engine/period.ts delete mode 100644 source/engine/period.tsx create mode 100644 test/period.test.js diff --git a/publicode/rules/dirigeant.yaml b/publicode/rules/dirigeant.yaml index ebe82861c..4eb449178 100644 --- a/publicode/rules/dirigeant.yaml +++ b/publicode/rules/dirigeant.yaml @@ -426,6 +426,15 @@ dirigeant . indépendant . revenu net de cotisations: dirigeant . indépendant . revenu professionnel: unité par défaut: €/an + titre: revenu professionnel (net imposable) + description: | + C'est le revenu net de cotisations déductibles du travailleur indépendant, qui sert de base au calcul des cotisations et de l'impôt pour les indépendants. + + Attention, **notre calcul est fait au régime de croisière**: + l'indépendant qui se lance paiera pendant ses 2 premières années un forfait relativement réduit de cotisations sociales. Il devra ensuite régulariser cette situation par rapport au revenu qu'il a vraiment perçu. + + Il faut donc voir ce calcul comme *le montant qui devra de toute façon être payé* à court terme après 2 ans d'exercice. + formule: inversion numérique: avec: @@ -437,7 +446,6 @@ dirigeant . indépendant . revenu professionnel: valeurs négatives possibles: oui dirigeant . indépendant . assiette des cotisations: - unité par défaut: €/an formule: encadrement: plancher: 0 diff --git a/source/engine/date.ts b/source/engine/date.ts index 69781b0e3..2d2a8260a 100644 --- a/source/engine/date.ts +++ b/source/engine/date.ts @@ -37,3 +37,13 @@ export function convertToDateIfNeeded(...values: string[]) { }) return dateStrings.map(convertToDate) } + +export function convertToString(date: Date): string { + return normalizeDate(date.getFullYear(), date.getMonth() + 1, date.getDate()) +} + +export function getRelativeDate(date: string, dayDifferential: number): string { + const relativeDate = new Date(convertToDate(date)) + relativeDate.setDate(relativeDate.getDate() + dayDifferential) + return convertToString(relativeDate) +} diff --git a/source/engine/error.ts b/source/engine/error.ts index 3a5a9d006..2283873ca 100644 --- a/source/engine/error.ts +++ b/source/engine/error.ts @@ -1,14 +1,16 @@ import { coerceArray } from '../utils' + +export class EngineError extends Error {} export function syntaxError( rules: string[] | string, message: string, originalError?: Error ) { - throw new Error( + throw new EngineError( `\n[ Erreur syntaxique ] -➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` -✖️ ${message} - ${originalError && originalError.message} +➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` +✖️ ${message} + ${originalError ? originalError.message : ''} ` ) } @@ -18,11 +20,11 @@ export function evaluationError( message: string, originalError?: Error ) { - throw new Error( + throw new EngineError( `\n[ Erreur d'évaluation ] -➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` -✖️ ${message} - ${originalError && originalError.message} +➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` +✖️ ${message} + ${originalError ? originalError.message : ''} ` ) } @@ -34,9 +36,9 @@ export function typeWarning( ) { console.warn( `\n[ Erreur de type ] -➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` -✖️ ${message} - ${originalError && originalError.message} +➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` +✖️ ${message} + ${originalError ? originalError.message : ''} ` ) } @@ -48,9 +50,9 @@ export function warning( ) { console.warn( `\n[ Avertissement ] -➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` -⚠️ ${message} -💡${solution} +➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` +⚠️ ${message} +💡 ${solution} ` ) } diff --git a/source/engine/evaluateRule.ts b/source/engine/evaluateRule.ts index daa9db1ce..491ec58a1 100644 --- a/source/engine/evaluateRule.ts +++ b/source/engine/evaluateRule.ts @@ -102,6 +102,7 @@ export default (cache, situationGate, parsedRules, node) => { node.defaultUnit || evaluatedFormula.unit + const temporalValue = evaluatedFormula.temporalValue if (unit) { try { nodeValue = convertNodeToUnit(unit, evaluatedFormula).nodeValue @@ -121,7 +122,7 @@ export default (cache, situationGate, parsedRules, node) => { ...(node.formule && { formule: evaluatedFormula }), nodeValue, unit, - period, + temporalValue, isApplicable, missingVariables } diff --git a/source/engine/evaluation.js b/source/engine/evaluation.tsx similarity index 82% rename from source/engine/evaluation.js rename to source/engine/evaluation.tsx index 4baa7719c..7a3f80440 100644 --- a/source/engine/evaluation.js +++ b/source/engine/evaluation.tsx @@ -12,9 +12,13 @@ import { reduce, values } from 'ramda' -import React from 'react' import { typeWarning } from './error' import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits' +import { + createTemporalValue, + mergeTemporalValuesWith, + periodAverage +} from './period' export let makeJsx = node => typeof node.jsx == 'function' @@ -70,10 +74,34 @@ export let evaluateArray = (reducer, start) => ( parsedRules, node ) => { - let evaluateOne = child => - evaluateNode(cache, situationGate, parsedRules, child), - explanation = map(evaluateOne, node.explanation), - [unit, values] = sameUnitValues( + const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) + const explanation = node.explanation.map(evaluate) + if (explanation.some(node => node.temporalValue)) { + const reducerWithNull = (value1, value2) => + value1 === null || value2 === null ? null : reducer(value1, value2) + const temporalValue = explanation.reduce((acc, node) => { + if (!node.temporalValue && !Array.isArray(acc)) { + return reducerWithNull(acc, node.nodeValue) + } + const temporalValue = + node.temporalValue ?? createTemporalValue(node.nodeValue) + const temporalAcc = Array.isArray(acc) ? acc : createTemporalValue(acc) + + return mergeTemporalValuesWith( + reducerWithNull, + temporalAcc, + temporalValue + ) + }, start) + return { + ...node, + nodeValue: periodAverage(temporalValue), + temporalValue, + explanation + } + } + + const [unit, values] = sameUnitValues( explanation, cache._meta.contextRule, node.name diff --git a/source/engine/grammar.ne b/source/engine/grammar.ne index 7d017d2f9..6839da02a 100644 --- a/source/engine/grammar.ne +++ b/source/engine/grammar.ne @@ -75,8 +75,7 @@ Negation -> "-" %space Parentheses {% unaryOperation('calculation') %} Parentheses -> - "(" AdditionSubstraction ")" {% ([,e]) => e %} - | "(" Negation ")" {% ([,e]) => e %} + "(" NumericValue ")" {% ([,e]) => e %} | NumericTerminal {% id %} Date -> diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index 1e915cdcd..80cc506a3 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -2,36 +2,12 @@ import { decompose } from 'Engine/mecanisms/utils' import variations from 'Engine/mecanisms/variations' import { convertNodeToUnit } from 'Engine/nodeUnits' import { inferUnit, isPercentUnit } from 'Engine/units' -import { - add, - any, - equals, - evolve, - is, - map, - max, - mergeWith, - min, - path, - pluck, - reduce, - subtract, - toPairs -} from 'ramda' +import { add, any, equals, evolve, is, map, max, mergeWith, min, path, pluck, reduce, subtract, toPairs } from 'ramda' import React from 'react' import { Trans } from 'react-i18next' import 'react-virtualized/styles.css' import { typeWarning } from './error' -import { - collectNodeMissing, - defaultNode, - evaluateArray, - evaluateNode, - evaluateObject, - makeJsx, - mergeAllMissing, - parseObject -} from './evaluation' +import { collectNodeMissing, defaultNode, evaluateArray, evaluateNode, evaluateObject, makeJsx, mergeAllMissing, parseObject } from './evaluation' import Allègement from './mecanismViews/Allègement' import { Node, SimpleRuleLink } from './mecanismViews/common' import InversionNumérique from './mecanismViews/InversionNumérique' diff --git a/source/engine/mecanisms/durée.tsx b/source/engine/mecanisms/durée.tsx index 7d9c1438f..d7de95d5f 100644 --- a/source/engine/mecanisms/durée.tsx +++ b/source/engine/mecanisms/durée.tsx @@ -1,4 +1,4 @@ -import { convertToDate, normalizeDate } from 'Engine/date' +import { convertToDate, convertToString } from 'Engine/date' import { defaultNode, evaluateNode, @@ -26,12 +26,7 @@ function MecanismDurée({ nodeValue, explanation, unit }) { ) } -const today = new Date() -const todayString = normalizeDate( - today.getFullYear(), - today.getMonth() + 1, - today.getDate() -) +const todayString = convertToString(new Date()) const objectShape = { depuis: defaultNode(todayString), diff --git a/source/engine/mecanisms/variableTemporelle.tsx b/source/engine/mecanisms/variableTemporelle.ts similarity index 52% rename from source/engine/mecanisms/variableTemporelle.tsx rename to source/engine/mecanisms/variableTemporelle.ts index f155406ea..13eecfdd8 100644 --- a/source/engine/mecanisms/variableTemporelle.tsx +++ b/source/engine/mecanisms/variableTemporelle.ts @@ -1,5 +1,10 @@ -import { bonus, evaluateNode, mergeMissing } from 'Engine/evaluation' -import { isValidPeriod, periodIntersection } from 'Engine/period' +import { evaluateNode } from 'Engine/evaluation' +import { + createTemporalValue, + narrowTemporalValue, + periodAverage +} from 'Engine/period' +import { TemporalValue } from './../period' function evaluate( cache: any, @@ -16,27 +21,22 @@ function evaluate( const start = node.period.start && evaluateAttribute(node.period.start) const end = node.period.end && evaluateAttribute(node.period.end) const explanation = evaluateAttribute(node.explanation) - const period = periodIntersection(explanation.period, { start, end }) - let nodeValue: any = null - let missingVariables = mergeMissing( - period.start?.missingVariables, - period.end?.missingVariables - ) - if (isValidPeriod(period)) { - nodeValue = explanation.nodeValue - missingVariables = mergeMissing( - bonus(missingVariables, true), - explanation.missingVariables - ) - } else { - nodeValue = false + const period = { + start: start?.nodeValue ?? null, + end: end?.nodeValue ?? null } + + const temporalValue = explanation.temporalValue + ? narrowTemporalValue(period, explanation.temporalValue) + : createTemporalValue(explanation.nodeValue, period) + // TODO explanation missingVariables / period missing variables + return { ...node, - nodeValue, - period, - explanation, - missingVariables + nodeValue: periodAverage(temporalValue as TemporalValue), + temporalValue, + period: { start, end }, + explanation } } diff --git a/source/engine/parse.tsx b/source/engine/parse.tsx index c84c73885..e82875715 100644 --- a/source/engine/parse.tsx +++ b/source/engine/parse.tsx @@ -26,7 +26,7 @@ import { subtract } from 'ramda' import React from 'react' -import { syntaxError } from './error' +import { EngineError, syntaxError } from './error' import grammar from './grammar.ne' import { mecanismAllOf, @@ -87,6 +87,17 @@ export const parseExpression = (rule, rawNode) => { } const parseMecanism = (rules, rule, parsedRules) => rawNode => { + if (Array.isArray(rawNode)) { + syntaxError( + rule.dottedName, + ` +Il manque le nom du mécanisme pour le tableau : [${rawNode + .map(x => `'${x}'`) + .join(', ')}] +Les mécanisme possibles sont : 'somme', 'le maximum de', 'le minimum de', 'toutes ces conditions', 'une de ces conditions'. + ` + ) + } if (Object.keys(rawNode).length > 1) { syntaxError( rule.dottedName, @@ -137,7 +148,14 @@ Le mécanisme ${mecanismName} est inconnu. Vérifiez qu'il n'y ait pas d'erreur dans l'orthographe du nom.` ) } - return parseFn(parse(rules, rule, parsedRules), mecanismName, values) + try { + return parseFn(parse(rules, rule, parsedRules), mecanismName, values) + } catch (e) { + if (e instanceof EngineError) { + throw e + } + syntaxError(rule.dottedName, e.message) + } } const knownOperations = { diff --git a/source/engine/parseReference.js b/source/engine/parseReference.js index df4fa7f2f..8f926be86 100644 --- a/source/engine/parseReference.js +++ b/source/engine/parseReference.js @@ -149,11 +149,11 @@ 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, period) => { + let cacheNode = (nodeValue, missingVariables, explanation, temporalValue) => { cache[cacheName] = { ...node, nodeValue, - period, + temporalValue, ...(explanation && { explanation }), @@ -185,7 +185,7 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v evaluation.nodeValue, evaluation.missingVariables, evaluation, - evaluation.period + evaluation.temporalValue ) } diff --git a/source/engine/parseRule.tsx b/source/engine/parseRule.tsx index aa22d1f54..ef9ab3ca6 100644 --- a/source/engine/parseRule.tsx +++ b/source/engine/parseRule.tsx @@ -83,14 +83,14 @@ export default (rules, rule, parsedRules) => { parsedRules, node.explanation ), - { nodeValue, unit, missingVariables, period } = explanation + { nodeValue, unit, missingVariables, temporalValue } = explanation return { ...node, nodeValue, unit, missingVariables, explanation, - period + temporalValue } } diff --git a/source/engine/period.ts b/source/engine/period.ts new file mode 100644 index 000000000..9437fa32e --- /dev/null +++ b/source/engine/period.ts @@ -0,0 +1,258 @@ +import { convertToDate, getRelativeDate } from 'Engine/date' + +export type Period = { + start: Date | null + end: Date | null +} + +export function parsePeriod(word: string, date: Date): Period { + const startWords = [ + 'depuis', + 'depuis le', + 'depuis la', + 'à partir de', + 'à partir du', + 'du' + ] + const endWords = [ + "jusqu'à", + "jusqu'au", + "jusqu'à la", + 'avant', + 'avant le', + 'avant la', + 'au' + ] + const intervalWords = ['le', 'en'] + if (!startWords.concat(endWords, intervalWords).includes(word)) { + throw new SyntaxError( + `Le mot clé '${word}' n'est pas valide. Les mots clés possible sont les suivants :\n\t ${startWords.join( + ', ' + )}` + ) + } + if (word === 'le') { + return { + start: date, + end: date + } + } + if (word === 'en') { + console.log(word, date) + return { start: null, end: null } + } + if (startWords.includes(word)) { + return { + start: date, + end: null + } + } + if (endWords.includes(word)) { + return { + start: null, + end: date + } + } + throw new Error('Non implémenté') +} + +// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable) +// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)] +type Evaluation = T | false | null +type EvaluatedNode = { + nodeValue: Evaluation + temporalValue?: TemporalValue> +} +export type TemporalValue = Array & { value: T }> + +export function narrowTemporalValue( + period: Period, + temporalValue: TemporalValue> +): TemporalValue> { + return mergeTemporalValuesWith( + (value, filter) => filter && value, + temporalValue, + createTemporalValue(true, period) + ) +} + +// Returns a temporal value that's true for the given period and false otherwise. +export function createTemporalValue( + value: Evaluation, + period: Period = { start: null, end: null } +): TemporalValue> { + let temporalValue = [{ ...period, value }] + if (period.start != null) { + temporalValue.unshift({ + start: null, + end: getRelativeDate(period.start, -1), + value: false + }) + } + if (period.end != null) { + temporalValue.push({ + start: getRelativeDate(period.end, 1), + end: null, + value: false + }) + } + return temporalValue +} + +export function mapTemporalValue( + fn: (value: T1) => T2, + temporalValue: TemporalValue +): TemporalValue { + return temporalValue.map(({ start, end, value }) => ({ + start, + end, + value: fn(value) + })) +} + +export function mergeTemporalValuesWith( + fn: (value1: T1, value2: T2) => T3, + temporalValue1: TemporalValue, + temporalValue2: TemporalValue +): TemporalValue { + return mapTemporalValue( + ([a, b]) => fn(a, b), + concatTemporalValues(temporalValue1, temporalValue2) + ) +} + +export function concatTemporalValues( + temporalValue1: TemporalValue, + temporalValue2: TemporalValue, + acc: TemporalValue<[T1, T2]> = [] +): TemporalValue<[T1, T2]> { + if (!temporalValue1.length && !temporalValue2.length) { + return acc + } + const [value1, ...rest1] = temporalValue1 + const [value2, ...rest2] = temporalValue2 + console.assert(value1.start === value2.start) + const endDateComparison = compareEndDate(value1.end, value2.end) + + // End dates are equals + if (endDateComparison === 0) { + return concatTemporalValues(rest1, rest2, [ + ...acc, + { ...value1, value: [value1.value, value2.value] } + ]) + } + // Value1 lasts longuer than value1 + if (endDateComparison > 0) { + console.assert(value2.end !== null) + return concatTemporalValues( + [ + { ...value1, start: getRelativeDate(value2.end as string, 1) }, + ...rest1 + ], + rest2, + [ + ...acc, + { + ...value2, + value: [value1.value, value2.value] + } + ] + ) + } + + // Value2 lasts longuer than value1 + if (endDateComparison < 0) { + console.assert(value1.end !== null) + return concatTemporalValues( + rest1, + [ + { ...value2, start: getRelativeDate(value1.end as string, 1) }, + ...rest2 + ], + [ + ...acc, + { + ...value1, + value: [value1.value, value2.value] + } + ] + ) + } + throw new EvalError('All case should have been covered') +} + +function simplify(temporalValue: TemporalValue): TemporalValue { + return temporalValue +} + +function compareStartDate( + dateA: string | null, + dateB: string | null +): -1 | 0 | 1 { + if (dateA == dateB) { + return 0 + } + if (dateA == null) { + return -1 + } + if (dateB == null) { + return 1 + } + return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1 +} + +function compareEndDate( + dateA: string | null, + dateB: string | null +): -1 | 0 | 1 { + if (dateA == dateB) { + return 0 + } + if (dateA == null) { + return 1 + } + if (dateB == null) { + return -1 + } + return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1 +} + +export function periodAverage( + temporalValue: TemporalValue> +): Evaluation { + temporalValue = temporalValue.filter(({ value }) => value !== false) + const first = temporalValue[0] + const last = temporalValue[temporalValue.length - 1] + if (!temporalValue.length) { + return false + } + + // La variable est définie sur un interval infini + if (first.start == null || last.end == null) { + if (first.start != null) { + return last.value + } + if (last.end != null) { + return first.value + } + return first.value + last.value / 2 + } + + if (temporalValue.some(({ value }) => value == null)) { + return null + } + let totalWeight = 0 + const weights = temporalValue.map(({ start, end, value }) => { + const day = 1000 * 60 * 60 * 24 + const weight = + convertToDate(end as string).getTime() - + convertToDate(start as string).getTime() + + day + totalWeight += weight + return (value as number) * weight + }) + return weights.reduce( + (average, weightedValue) => average + weightedValue / totalWeight, + 0 + ) +} diff --git a/source/engine/period.tsx b/source/engine/period.tsx deleted file mode 100644 index 09c260563..000000000 --- a/source/engine/period.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { convertToDate } from 'Engine/date' - -export type Period = { - start: Date | null - end: Date | null -} - -export function parsePeriod(word: string, date: Date): Period { - const startWords = [ - 'depuis', - 'depuis le', - 'depuis la', - 'à partir de', - 'à partir du', - 'du' - ] - const endWords = [ - "jusqu'à", - "jusqu'au", - "jusqu'à la", - 'avant', - 'avant le', - 'avant la', - 'au' - ] - const intervalWords = ['le'] - if (!startWords.concat(endWords, intervalWords).includes(word)) { - throw new SyntaxError( - `Le mot clé '${word}' n'est pas valide. Les mots clés possible sont les suivants :\n\t ${startWords.join( - ', ' - )}` - ) - } - if (word === 'le') { - return { - start: date, - end: date - } - } - if (startWords.includes(word)) { - return { - start: date, - end: null - } - } - if (endWords.includes(word)) { - return { - start: null, - end: date - } - } - throw new Error('Non implémenté') -} - -export function periodIntersection( - parentPeriod: Period<{ nodeValue: string }> | null, - childPeriod: Period<{ nodeValue: string }> | null -): Period<{ nodeValue: string }> { - if (!parentPeriod) { - return childPeriod || { start: null, end: null } - } - if (!childPeriod) { - return parentPeriod || { start: null, end: null } - } - const startDateParent = - parentPeriod.start?.nodeValue && convertToDate(parentPeriod.start.nodeValue) - const startDateChild = - childPeriod.start?.nodeValue && convertToDate(childPeriod.start.nodeValue) - const endDateParent = - parentPeriod.end?.nodeValue && convertToDate(parentPeriod.end.nodeValue) - const endDateChild = - childPeriod.end?.nodeValue && convertToDate(childPeriod.end.nodeValue) - return { - start: - startDateChild == null || startDateParent > startDateChild - ? parentPeriod.start - : childPeriod.start, - end: - endDateParent == null || endDateParent > endDateChild - ? childPeriod.end - : parentPeriod.end - } -} - -export function isValidPeriod(period: Period<{ nodeValue: string }>): boolean { - return ( - period.start?.nodeValue == null || - period.end?.nodeValue == null || - convertToDate(period.end.nodeValue) >= convertToDate(period.start.nodeValue) - ) -} diff --git a/source/engine/rules.js b/source/engine/rules.js index 83588f66a..0df4a87eb 100644 --- a/source/engine/rules.js +++ b/source/engine/rules.js @@ -1,6 +1,30 @@ import { parseUnit } from 'Engine/units' import rawRules from 'Publicode/rules' -import { assoc, chain, dropLast, filter, fromPairs, is, isNil, join, last, map, path, pipe, propEq, props, range, reduce, reduced, reject, split, take, toPairs, trim, when } from 'ramda' +import { + assoc, + chain, + dropLast, + filter, + fromPairs, + is, + isNil, + join, + last, + map, + path, + pipe, + propEq, + props, + range, + reduce, + reduced, + reject, + split, + take, + toPairs, + trim, + when +} from 'ramda' import translations from '../locales/rules-en.yaml' // TODO - should be in UI, not engine import { capitalise0, coerceArray } from '../utils' @@ -89,7 +113,7 @@ export let ruleParents = dottedName => { */ export let disambiguateRuleReference = ( allRules, - { dottedName, name }, + { dottedName }, partialName ) => { let pathPossibilities = [ @@ -113,9 +137,8 @@ export let disambiguateRuleReference = ( return found.dottedName } - throw new Error( - `OUUUUPS la référence '${partialName}' dans la règle '${name}' est introuvable dans la base` - ) + throw new Error(`La référence '${partialName}' est introuvable. +Vérifiez que l'orthographe et l'espace de nom sont corrects`) } export let collectDefaults = pipe( diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml index de100cf24..343e69c6f 100644 --- a/test/mécanismes/variable-temporelle.yaml +++ b/test/mécanismes/variable-temporelle.yaml @@ -47,34 +47,51 @@ variable temporelle numérique . intervalle . test date non applicable 2: exemples: - valeur attendue: false -variable temporelle numérique . date limite de paiement: +variable temporelle numérique . variable . date limite de paiement: formule: 03/09/2020 -variable temporelle numérique . majorations de retard: +variable temporelle numérique . variable . majorations de retard: formule: '40 €/jour | à partir de : date limite de paiement' -variable temporelle numérique . test date non applicable: +variable temporelle numérique . variable . test date non applicable: formule: "majorations de retard | jusqu'au : 02/09/2020" exemples: - valeur attendue: false -# variable temporelle numérique . test date non applicable 2: -# formule: majorations de retard | du 01/02/2020 | au 03/08/2020 -# exemples: -# - valeur attendue: false -# variable temporelle numérique . test date applicable: -# formule: 'majorations de retard | depuis la : date limite de paiement' -# exemples: -# - valeur attendue: 40 +variable temporelle numérique . variable . test date non applicable 2: + formule: majorations de retard | du 01/02/2020 | au 03/08/2020 + exemples: + - valeur attendue: false -# variable temporelle numérique . test date applicable 2: -# formule: majorations de retard | le 03/09/2020 -# exemples: -# - valeur attendue: 40 -# variable temporelle numérique . test associativité: -# formule: valeur + 120 €/an | le 02/09/2019 -# exemples: -# - valeur attendue: 50 +variable temporelle numérique . variable . test date applicable: + formule: 'majorations de retard | depuis la : date limite de paiement' + exemples: + - valeur attendue: 40 + +variable temporelle numérique . variable . test date applicable 2: + formule: majorations de retard | le 03/09/2020 + exemples: + - valeur attendue: 40 + +variable temporelle numérique . addition . valeur: + formule: (20 €/mois | à partir du 15/11/2019) + (10 €/mois | à partir du 12/09/2020) + +date: +variable temporelle numérique . addition . test date: + formule: 'valeur | le : date' + exemples: + - situation: + date: 01/01/2019 + valeur attendue: false + - situation: + date: 15/12/2019 + valeur attendue: 20 + - situation: + date: 12/09/2020 + valeur attendue: 30 + +# variable temporelle numérique . somme . valeur 2: +# formule: valeur - (120 €/an | jusqu'au 04/03/2020) # variable temporelle numérique . test date non applicable: # formule: valeur # exemples: @@ -91,31 +108,41 @@ variable temporelle numérique . test date non applicable: # exemples: # - valeur attendue: 40 -# contrat salarié . date d'embauche: -# type: date -# formule: 25/04/2018 +contrat salarié . date d'embauche: + formule: 15/04/2019 -# contrat salarié . salaire . brut de base: +contrat salarié . salaire . brut de base: + formule: + somme: + - "2000€/mois | depuis : date d'embauche | jusqu'au 08/08/2019" + - 2200€/mois | depuis le 09/08/2019 + +contrat salarié . salaire . primes: + formule: 2000€/mois | du 01/12/2019 | au 31/12/2019 + +contrat salarié . salaire . brut: + formule: + somme: + - brut de base + - primes + +contrat salarié . PSS proratisé: + formule: "3377 €/mois | depuis : date d'embauche" + +# contrat salarié . cotisations . retraite: # formule: -# calendrier: -# - depuis: date d'embauche -# montant: 2000€/mois -# - depuis: 09/08/2019 -# montant: 2200€/mois +# multiplication: +# assiette: salaire . brut +# taux: 13% +# plafond: PSS proratisé -# salaire . primes: -# formule: -# calendrier: -# - en: décembre 2019 -# montant: 2000€/mois +variable temporelle numérique . somme: + formule: contrat salarié . salaire . brut | du 01/12/2019 | au 31/12/2019 + exemples: + - valeur attendue: 4200 # 2000 + 2200 -# salaire . brut: -# formules: -# - brut de base -# - primes - -# test . calcul brut et prime: -# formule: salaire brut [décembre 2019] +# variable temporelle numérique . multiplication: +# formule: contrat salarié . cotisation . retraite | du 01/12/2019 | au 31/12/2019 # exemples: # - valeur attendue: 4200 # 2000 + 2200 @@ -129,25 +156,17 @@ variable temporelle numérique . test date non applicable: # exemples: # - valeur attendue: 2148.39 # (2000 * 8 + 2200 * 23)/31 -# PSS: -# formule: -# calendrier: -# - en: 2019 -# montant: 3377 €/mois -# - en: 2020 -# montant: 3428 €/mois - -# contrat salarié . PSS proratisé: -# période: -# depuis: date d'embauche -# formule: PSS - -# test . proratisation du PSS pour le mois d'embauche: -# période: -# en: avril 2019 -# formule: PSS proratisé +# variable temporelle numérique . test proratisation du PSS pour le mois d'embauche: +# formule: contrat salarié . PSS proratisé | du 01/04/2019 | au 30/04/2019 # exemples: -# - valeur attendue: 2183 +# - valeur attendue: 1688.5 +# cotisations . tranche 1 . montant : +# formule: +# PSS - salaire brut + +# cotisations . tranche 1 . régularisation : +# formule: +# '(PSS - salaire brut) | depuis le 01/01/2020' # cotisations . retraite employeur plafonnée: # formule: diff --git a/test/period.test.js b/test/period.test.js new file mode 100644 index 000000000..23c08cf42 --- /dev/null +++ b/test/period.test.js @@ -0,0 +1,70 @@ +import { expect } from 'chai' +import { + concatTemporalValues, + createTemporalValue, + mergeTemporalValuesWith +} from '../source/engine/period' + +const neverEnding = value => [{ start: null, end: null, value: value }] +describe('Periods : concat', () => { + it('should concat two empty temporalValue', () => { + const result = concatTemporalValues([], []) + expect(result).to.deep.equal([]) + }) + + it('should concat constant never-ending temporalValue', () => { + const result = concatTemporalValues(neverEnding(1), neverEnding(2)) + expect(result).to.deep.equal(neverEnding([1, 2])) + }) + + it('should concat changing never-ending temporalValue', () => { + const value1 = createTemporalValue(true, { start: null, end: '01/08/2020' }) + const value2 = neverEnding(1) + expect(concatTemporalValues(value1, value2)).to.deep.equal([ + { start: null, end: '01/08/2020', value: [true, 1] }, + { start: '02/08/2020', end: null, value: [false, 1] } + ]) + expect(concatTemporalValues(value2, value1)).to.deep.equal([ + { start: null, end: '01/08/2020', value: [1, true] }, + { start: '02/08/2020', end: null, value: [1, false] } + ]) + }) + + it('should concat two overlapping never-ending temporalValue', () => { + const value1 = createTemporalValue(1, { + start: '01/07/2019', + end: '30/06/2020' + }) + const value2 = createTemporalValue(2, { + start: '01/01/2019', + end: '31/12/2019' + }) + + expect(concatTemporalValues(value1, value2)).to.deep.equal([ + { start: null, end: '31/12/2018', value: [false, false] }, + { start: '01/01/2019', end: '30/06/2019', value: [false, 2] }, + { start: '01/07/2019', end: '31/12/2019', value: [1, 2] }, + { start: '01/01/2020', end: '30/06/2020', value: [1, false] }, + { start: '01/07/2020', end: null, value: [false, false] } + ]) + }) +}) + +describe('Periods : merge', () => { + it('should merge two overlapping never-ending temporalValue', () => { + const value1 = createTemporalValue(10) + const value2 = [ + { start: null, end: '14/04/2019', value: 100 }, + { start: '15/04/2019', end: '08/08/2019', value: 2000 }, + { start: '09/08/2019', end: null, value: 200 } + ] + + expect( + mergeTemporalValuesWith((a, b) => a + b, value1, value2) + ).to.deep.equal([ + { start: null, end: '14/04/2019', value: 110 }, + { start: '15/04/2019', end: '08/08/2019', value: 2010 }, + { start: '09/08/2019', end: null, value: 210 } + ]) + }) +}) From 0a5aba90784881e69b0d1b8b090efbc650fe185d Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Mon, 24 Feb 2020 16:41:19 +0100 Subject: [PATCH 05/13] :gear: ajoute la gestion des variables temporelles sur les multiplications --- source/engine/evaluation.tsx | 124 +++++++++--------- source/engine/mecanisms.js | 28 +++- source/engine/mecanisms/variableTemporelle.ts | 9 +- source/engine/period.ts | 84 ++++++++---- test/mécanismes/variable-temporelle.yaml | 63 ++++++--- test/period.test.js | 51 +++---- 6 files changed, 223 insertions(+), 136 deletions(-) diff --git a/source/engine/evaluation.tsx b/source/engine/evaluation.tsx index 7a3f80440..4ef7bfc14 100644 --- a/source/engine/evaluation.tsx +++ b/source/engine/evaluation.tsx @@ -5,19 +5,20 @@ import { evolve, filter, fromPairs, - is, keys, map, mergeWith, - reduce, - values + reduce } from 'ramda' import { typeWarning } from './error' import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits' import { - createTemporalValue, - mergeTemporalValuesWith, - periodAverage + concatTemporals, + liftTemporalNode, + mapTemporal, + periodAverage, + pure, + zipTemporals } from './period' export let makeJsx = node => @@ -75,48 +76,37 @@ export let evaluateArray = (reducer, start) => ( node ) => { const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) - const explanation = node.explanation.map(evaluate) - if (explanation.some(node => node.temporalValue)) { - const reducerWithNull = (value1, value2) => - value1 === null || value2 === null ? null : reducer(value1, value2) - const temporalValue = explanation.reduce((acc, node) => { - if (!node.temporalValue && !Array.isArray(acc)) { - return reducerWithNull(acc, node.nodeValue) - } - const temporalValue = - node.temporalValue ?? createTemporalValue(node.nodeValue) - const temporalAcc = Array.isArray(acc) ? acc : createTemporalValue(acc) - - return mergeTemporalValuesWith( - reducerWithNull, - temporalAcc, - temporalValue - ) - }, start) - return { - ...node, - nodeValue: periodAverage(temporalValue), - temporalValue, - explanation - } - } - - const [unit, values] = sameUnitValues( + const temporalExplanation = concatTemporals( + node.explanation.map(evaluate).map(liftTemporalNode) + ) + const temporalEvaluations = mapTemporal(explanation => { + explanation + const [unit, values] = sameUnitValues( explanation, cache._meta.contextRule, node.name - ), - nodeValue = values.some(value => value === null) + ) + const nodeValue = values.some(value => value === null) ? null - : reduce(reducer, start, values), - missingVariables = + : reduce(reducer, start, values) + const missingVariables = node.nodeValue == null ? mergeAllMissing(explanation) : {} + return { + ...node, + nodeValue, + explanation, + missingVariables, + unit + } + }, temporalExplanation) + if (temporalEvaluations.length === 1) { + return temporalEvaluations[0] + } + const temporalValue = mapTemporal(node => node.nodeValue, temporalEvaluations) return { ...node, - nodeValue, - explanation, - missingVariables, - unit + temporalValue, + nodeValue: periodAverage(temporalValue) } } @@ -171,28 +161,42 @@ export let evaluateObject = (objectShape, effect) => ( parsedRules, node ) => { - let evaluateOne = child => - evaluateNode(cache, situationGate, parsedRules, child) + const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) + const evaluations = map(evaluate, node.explanation) + const temporalExplanations = mapTemporal( + Object.fromEntries, + concatTemporals( + Object.entries(evaluations).map(([key, node]) => + zipTemporals(pure(key), liftTemporalNode(node)) + ) + ) + ) + const temporalEvaluations = mapTemporal( + explanations => effect(explanations, cache, situationGate, parsedRules), + temporalExplanations + ) + + const temporalValue = mapTemporal( + evaluation => + evaluation !== null && typeof evaluation === 'object' + ? evaluation.nodeValue + : evaluation, + temporalEvaluations + ) + const nodeValue = periodAverage(temporalValue) - let transforms = map(k => [k, evaluateOne], keys(objectShape)), - automaticExplanation = evolve(fromPairs(transforms))(node.explanation) - // the result of effect can either be just a nodeValue, or an object {additionalExplanation, nodeValue}. The latter is useful for a richer JSX visualisation of the mecanism : the view should not duplicate code to recompute intermediate values (e.g. for a marginal 'barème', the marginal 'tranche') - let evaluated = effect( - automaticExplanation, - cache, - situationGate, - parsedRules - ), - explanation = is(Object, evaluated) - ? { ...automaticExplanation, ...evaluated.additionalExplanation } - : automaticExplanation, - nodeValue = is(Object, evaluated) ? evaluated.nodeValue : evaluated, - missingVariables = mergeAllMissing(values(explanation)) return simplifyNodeUnit({ ...node, nodeValue, - explanation, - missingVariables, - unit: explanation.unit + ...(temporalEvaluations.length > 1 + ? { temporalValue } + : { + missingVariables: mergeAllMissing(Object.values(evaluations)), + explanation: { + ...evaluations, + ...temporalEvaluations[0].additionalExplanation + }, + unit: temporalEvaluations[0].unit + }) }) } diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index 80cc506a3..1e915cdcd 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -2,12 +2,36 @@ import { decompose } from 'Engine/mecanisms/utils' import variations from 'Engine/mecanisms/variations' import { convertNodeToUnit } from 'Engine/nodeUnits' import { inferUnit, isPercentUnit } from 'Engine/units' -import { add, any, equals, evolve, is, map, max, mergeWith, min, path, pluck, reduce, subtract, toPairs } from 'ramda' +import { + add, + any, + equals, + evolve, + is, + map, + max, + mergeWith, + min, + path, + pluck, + reduce, + subtract, + toPairs +} from 'ramda' import React from 'react' import { Trans } from 'react-i18next' import 'react-virtualized/styles.css' import { typeWarning } from './error' -import { collectNodeMissing, defaultNode, evaluateArray, evaluateNode, evaluateObject, makeJsx, mergeAllMissing, parseObject } from './evaluation' +import { + collectNodeMissing, + defaultNode, + evaluateArray, + evaluateNode, + evaluateObject, + makeJsx, + mergeAllMissing, + parseObject +} from './evaluation' import Allègement from './mecanismViews/Allègement' import { Node, SimpleRuleLink } from './mecanismViews/common' import InversionNumérique from './mecanismViews/InversionNumérique' diff --git a/source/engine/mecanisms/variableTemporelle.ts b/source/engine/mecanisms/variableTemporelle.ts index 13eecfdd8..90c9eb748 100644 --- a/source/engine/mecanisms/variableTemporelle.ts +++ b/source/engine/mecanisms/variableTemporelle.ts @@ -1,10 +1,10 @@ import { evaluateNode } from 'Engine/evaluation' import { - createTemporalValue, + createTemporalEvaluation, narrowTemporalValue, periodAverage } from 'Engine/period' -import { TemporalValue } from './../period' +import { Temporal } from './../period' function evaluate( cache: any, @@ -18,6 +18,7 @@ function evaluate( situation, parsedRules ) + const start = node.period.start && evaluateAttribute(node.period.start) const end = node.period.end && evaluateAttribute(node.period.end) const explanation = evaluateAttribute(node.explanation) @@ -28,12 +29,12 @@ function evaluate( const temporalValue = explanation.temporalValue ? narrowTemporalValue(period, explanation.temporalValue) - : createTemporalValue(explanation.nodeValue, period) + : createTemporalEvaluation(explanation.nodeValue, period) // TODO explanation missingVariables / period missing variables return { ...node, - nodeValue: periodAverage(temporalValue as TemporalValue), + nodeValue: periodAverage(temporalValue as Temporal), temporalValue, period: { start, end }, explanation diff --git a/source/engine/period.ts b/source/engine/period.ts index 9437fa32e..0a6771add 100644 --- a/source/engine/period.ts +++ b/source/engine/period.ts @@ -59,28 +59,31 @@ export function parsePeriod(word: string, date: Date): Period { // 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)] type Evaluation = T | false | null + type EvaluatedNode = { nodeValue: Evaluation - temporalValue?: TemporalValue> + temporalValue?: Temporal> } -export type TemporalValue = Array & { value: T }> + +type TemporalNode = Temporal<{ nodeValue: Evaluation }> +export type Temporal = Array & { value: T }> export function narrowTemporalValue( period: Period, - temporalValue: TemporalValue> -): TemporalValue> { - return mergeTemporalValuesWith( + temporalValue: Temporal> +): Temporal> { + return liftTemporal2( (value, filter) => filter && value, temporalValue, - createTemporalValue(true, period) + createTemporalEvaluation(true, period) ) } // Returns a temporal value that's true for the given period and false otherwise. -export function createTemporalValue( +export function createTemporalEvaluation( value: Evaluation, period: Period = { start: null, end: null } -): TemporalValue> { +): Temporal> { let temporalValue = [{ ...period, value }] if (period.start != null) { temporalValue.unshift({ @@ -99,10 +102,14 @@ export function createTemporalValue( return temporalValue } -export function mapTemporalValue( +export function pure(value: T): Temporal { + return [{ start: null, end: null, value }] +} + +export function mapTemporal( fn: (value: T1) => T2, - temporalValue: TemporalValue -): TemporalValue { + temporalValue: Temporal +): Temporal { return temporalValue.map(({ start, end, value }) => ({ start, end, @@ -110,22 +117,45 @@ export function mapTemporalValue( })) } -export function mergeTemporalValuesWith( +function liftTemporal2( fn: (value1: T1, value2: T2) => T3, - temporalValue1: TemporalValue, - temporalValue2: TemporalValue -): TemporalValue { - return mapTemporalValue( + temporalValue1: Temporal, + temporalValue2: Temporal +): Temporal { + return mapTemporal( ([a, b]) => fn(a, b), - concatTemporalValues(temporalValue1, temporalValue2) + zipTemporals(temporalValue1, temporalValue2) ) } -export function concatTemporalValues( - temporalValue1: TemporalValue, - temporalValue2: TemporalValue, - acc: TemporalValue<[T1, T2]> = [] -): TemporalValue<[T1, T2]> { +export function concatTemporals( + temporalValues: Array> +): Temporal> { + return temporalValues.reduce( + (values, value) => liftTemporal2((a, b) => [...a, b], values, value), + pure([]) as Temporal> + ) +} + +export function liftTemporalNode(node: EvaluatedNode): TemporalNode { + const { temporalValue, ...baseNode } = node + if (!temporalValue) { + return pure(baseNode) + } + return mapTemporal( + nodeValue => ({ + ...baseNode, + nodeValue + }), + temporalValue + ) +} + +export function zipTemporals( + temporalValue1: Temporal, + temporalValue2: Temporal, + acc: Temporal<[T1, T2]> = [] +): Temporal<[T1, T2]> { if (!temporalValue1.length && !temporalValue2.length) { return acc } @@ -136,7 +166,7 @@ export function concatTemporalValues( // End dates are equals if (endDateComparison === 0) { - return concatTemporalValues(rest1, rest2, [ + return zipTemporals(rest1, rest2, [ ...acc, { ...value1, value: [value1.value, value2.value] } ]) @@ -144,7 +174,7 @@ export function concatTemporalValues( // Value1 lasts longuer than value1 if (endDateComparison > 0) { console.assert(value2.end !== null) - return concatTemporalValues( + return zipTemporals( [ { ...value1, start: getRelativeDate(value2.end as string, 1) }, ...rest1 @@ -163,7 +193,7 @@ export function concatTemporalValues( // Value2 lasts longuer than value1 if (endDateComparison < 0) { console.assert(value1.end !== null) - return concatTemporalValues( + return zipTemporals( rest1, [ { ...value2, start: getRelativeDate(value1.end as string, 1) }, @@ -181,7 +211,7 @@ export function concatTemporalValues( throw new EvalError('All case should have been covered') } -function simplify(temporalValue: TemporalValue): TemporalValue { +function simplify(temporalValue: Temporal): Temporal { return temporalValue } @@ -218,7 +248,7 @@ function compareEndDate( } export function periodAverage( - temporalValue: TemporalValue> + temporalValue: Temporal> ): Evaluation { temporalValue = temporalValue.filter(({ value }) => value !== false) const first = temporalValue[0] diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml index 343e69c6f..6e5d6bc69 100644 --- a/test/mécanismes/variable-temporelle.yaml +++ b/test/mécanismes/variable-temporelle.yaml @@ -109,7 +109,13 @@ variable temporelle numérique . addition . test date: # - valeur attendue: 40 contrat salarié . date d'embauche: - formule: 15/04/2019 + formule: 12/09/2018 + +contrat salarié . salaire: + formule: + somme: + - brut de base + - primes contrat salarié . salaire . brut de base: formule: @@ -120,32 +126,53 @@ contrat salarié . salaire . brut de base: contrat salarié . salaire . primes: formule: 2000€/mois | du 01/12/2019 | au 31/12/2019 -contrat salarié . salaire . brut: +plafond sécurité sociale: formule: somme: - - brut de base - - primes + - 3377 €/mois | du 01/01/2019 | au 31/12/2019 + - 3424 €/mois | du 01/01/2020 | au 31/12/2020 -contrat salarié . PSS proratisé: - formule: "3377 €/mois | depuis : date d'embauche" - -# contrat salarié . cotisations . retraite: -# formule: -# multiplication: -# assiette: salaire . brut -# taux: 13% -# plafond: PSS proratisé +contrat salarié . cotisations . retraite: + formule: + multiplication: + assiette: salaire + plafond: plafond sécurité sociale + taux: 10% variable temporelle numérique . somme: - formule: contrat salarié . salaire . brut | du 01/12/2019 | au 31/12/2019 + formule: contrat salarié . salaire | du 01/12/2019 | au 31/12/2019 exemples: - valeur attendue: 4200 # 2000 + 2200 -# variable temporelle numérique . multiplication: -# formule: contrat salarié . cotisation . retraite | du 01/12/2019 | au 31/12/2019 -# exemples: -# - valeur attendue: 4200 # 2000 + 2200 +variable temporelle numérique . somme avec valeur changeant au cours du mois: + formule: contrat salarié . salaire | du 01/08/2019 | au 31/08/2019 + exemples: + - valeur attendue: 2148.387 # (2000 * 8 + 2200 * 23)/31 +variable temporelle numérique . multiplication: + formule: contrat salarié . cotisations . retraite | du 01/05/2019 | au 31/05/2019 + exemples: + - valeur attendue: 200 # 2000 * 10% + +variable temporelle numérique . multiplication avec valeur changeant au cours du mois: + formule: contrat salarié . cotisations . retraite | du 01/08/2019 | au 31/08/2019 + exemples: + - valeur attendue: 214.839 # (2000 * 8 + 2200 * 23)/31 + +variable temporelle numérique . multiplication avec valeur au dessus du plafond: + formule: contrat salarié . cotisations . retraite | du 01/12/2019 | au 31/12/2019 + exemples: + - valeur attendue: 337.7 # (2000 * 8 + 2200 * 23)/31 + +# variable temporelle numérique . multiplication avec valeur sur l'année: +# formule: contrat salarié . cotisations . retraite | du 01/01/2019 | au 31/12/2019 +# exemples: +# # 200 * 6 [janvier-juin] +# # + 214.839 [juillet] +# # + 220 * 4 [aout-novembre] +# # + 337.7 [décembre] +# # /12 mois +# - valeur attendue: 219.378 # test . proratisation du salaire avec entrée en cours de mois: # formule: salaire brut [avril 2019] # exemples: diff --git a/test/period.test.js b/test/period.test.js index 23c08cf42..014231d30 100644 --- a/test/period.test.js +++ b/test/period.test.js @@ -1,46 +1,49 @@ import { expect } from 'chai' import { - concatTemporalValues, - createTemporalValue, - mergeTemporalValuesWith + concatTemporals, + createTemporalEvaluation, + zipTemporals } from '../source/engine/period' const neverEnding = value => [{ start: null, end: null, value: value }] -describe('Periods : concat', () => { - it('should concat two empty temporalValue', () => { - const result = concatTemporalValues([], []) +describe('Periods : zip', () => { + it('should zip two empty temporalValue', () => { + const result = zipTemporals([], []) expect(result).to.deep.equal([]) }) - it('should concat constant never-ending temporalValue', () => { - const result = concatTemporalValues(neverEnding(1), neverEnding(2)) + it('should zip constant temporalValue', () => { + const result = zipTemporals(neverEnding(1), neverEnding(2)) expect(result).to.deep.equal(neverEnding([1, 2])) }) - it('should concat changing never-ending temporalValue', () => { - const value1 = createTemporalValue(true, { start: null, end: '01/08/2020' }) + it('should zip changing temporalValue', () => { + const value1 = createTemporalEvaluation(true, { + start: null, + end: '01/08/2020' + }) const value2 = neverEnding(1) - expect(concatTemporalValues(value1, value2)).to.deep.equal([ + expect(zipTemporals(value1, value2)).to.deep.equal([ { start: null, end: '01/08/2020', value: [true, 1] }, { start: '02/08/2020', end: null, value: [false, 1] } ]) - expect(concatTemporalValues(value2, value1)).to.deep.equal([ + expect(zipTemporals(value2, value1)).to.deep.equal([ { start: null, end: '01/08/2020', value: [1, true] }, { start: '02/08/2020', end: null, value: [1, false] } ]) }) - it('should concat two overlapping never-ending temporalValue', () => { - const value1 = createTemporalValue(1, { + it('should zip two overlapping temporalValue', () => { + const value1 = createTemporalEvaluation(1, { start: '01/07/2019', end: '30/06/2020' }) - const value2 = createTemporalValue(2, { + const value2 = createTemporalEvaluation(2, { start: '01/01/2019', end: '31/12/2019' }) - expect(concatTemporalValues(value1, value2)).to.deep.equal([ + expect(zipTemporals(value1, value2)).to.deep.equal([ { start: null, end: '31/12/2018', value: [false, false] }, { start: '01/01/2019', end: '30/06/2019', value: [false, 2] }, { start: '01/07/2019', end: '31/12/2019', value: [1, 2] }, @@ -50,21 +53,19 @@ describe('Periods : concat', () => { }) }) -describe('Periods : merge', () => { - it('should merge two overlapping never-ending temporalValue', () => { - const value1 = createTemporalValue(10) +describe('Periods : concat', () => { + it('should merge concat overlapping temporalValue', () => { + const value1 = createTemporalEvaluation(10) const value2 = [ { start: null, end: '14/04/2019', value: 100 }, { start: '15/04/2019', end: '08/08/2019', value: 2000 }, { start: '09/08/2019', end: null, value: 200 } ] - expect( - mergeTemporalValuesWith((a, b) => a + b, value1, value2) - ).to.deep.equal([ - { start: null, end: '14/04/2019', value: 110 }, - { start: '15/04/2019', end: '08/08/2019', value: 2010 }, - { start: '09/08/2019', end: null, value: 210 } + expect(concatTemporals([value1, value2])).to.deep.equal([ + { start: null, end: '14/04/2019', value: [10, 100] }, + { start: '15/04/2019', end: '08/08/2019', value: [10, 2000] }, + { start: '09/08/2019', end: null, value: [10, 200] } ]) }) }) From 665943288a3fbdc12768e42bbc432b5d999f4dc0 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Mon, 24 Feb 2020 18:34:38 +0100 Subject: [PATCH 06/13] =?UTF-8?q?:gear:=20Ajoute=20le=20m=C3=A9canisme=20r?= =?UTF-8?q?=C3=A9gularisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - améliore la gestion des unités pour les variables temporelles --- publicode/rules/salarié.yaml | 5 - source/engine/date.ts | 38 +++- source/engine/evaluation.tsx | 169 +++++++++--------- source/engine/mecanisms.js | 66 +------ source/engine/mecanisms/régularisation.ts | 150 ++++++++++++++++ source/engine/mecanisms/variableTemporelle.ts | 41 +++-- source/engine/parse.tsx | 2 + source/engine/period.ts | 167 ++++++++++++++--- test/date.test.js | 19 ++ test/mécanismes/régularisation.yaml | 40 +++++ test/mécanismes/variable-temporelle.yaml | 18 +- test/period.test.js | 61 +++++++ 12 files changed, 587 insertions(+), 189 deletions(-) create mode 100644 source/engine/mecanisms/régularisation.ts create mode 100644 test/date.test.js create mode 100644 test/mécanismes/régularisation.yaml diff --git a/publicode/rules/salarié.yaml b/publicode/rules/salarié.yaml index 7138a0e3e..c8685d5f9 100644 --- a/publicode/rules/salarié.yaml +++ b/publicode/rules/salarié.yaml @@ -1876,11 +1876,6 @@ contrat salarié . statut JEI . exonération de cotisations: - vieillesse .employeur contrat salarié . réduction générale: - aide: - type: réduction de cotisations - thème: aide bas salaires - démarches: non - alias: réduction fillon description: | Dans le cadre du pacte de responsabilité et de solidarité, le dispositif zéro cotisation Urssaf permet à l'employeur d'un salarié au Smic de ne plus payer aucune cotisation. Le montant de l'allègement est égal au produit de la rémunération annuelle brute par un coefficient. Il n'y a pas de formalité particulière à effectuer. références: diff --git a/source/engine/date.ts b/source/engine/date.ts index 2d2a8260a..04bf8b824 100644 --- a/source/engine/date.ts +++ b/source/engine/date.ts @@ -21,7 +21,11 @@ export function normalizeDate( const dateRegexp = /[\d]{2}\/[\d]{2}\/[\d]{4}/ export function convertToDate(value: string): Date { const [day, month, year] = normalizeDateString(value).split('/') - return new Date(+year, +month - 1, +day) + var result = new Date(+year, +month - 1, +day) + // Reset date to utc midnight for exact calculation of day difference (no + // daylight saving effect) + result.setMinutes(result.getMinutes() - result.getTimezoneOffset()) + return result } export function convertToDateIfNeeded(...values: string[]) { const dateStrings = values.map(dateString => '' + dateString) @@ -47,3 +51,35 @@ export function getRelativeDate(date: string, dayDifferential: number): string { relativeDate.setDate(relativeDate.getDate() + dayDifferential) return convertToString(relativeDate) } + +export function getYear(date: string): number { + return +date.slice(-4) +} + +export function getDifferenceInDays(from: string, to: string): number { + const millisecondsPerDay = 1000 * 60 * 60 * 24 + return ( + 1 + + (convertToDate(from).getTime() - convertToDate(to).getTime()) / + millisecondsPerDay + ) +} + +export function getDifferenceInMonths(from: string, to: string): number { + // We want to compute the difference in actual month between the two dates + // For date that start during a month, a pro-rata will be done depending on + // the duration of the month in days + const [dayFrom, monthFrom, yearFrom] = from.split('/').map(x => +x) + const [dayTo, monthTo, yearTo] = to.split('/').map(x => +x) + const numberOfFullMonth = monthTo - monthFrom + 12 * (yearTo - yearFrom) + const numDayMonthFrom = new Date(yearFrom, monthFrom, 0).getDate() + const numDayMonthTo = new Date(yearTo, monthTo, 0).getDate() + const prorataMonthFrom = (dayFrom - 1) / numDayMonthFrom + const prorataMonthTo = dayTo / numDayMonthTo + return numberOfFullMonth - prorataMonthFrom + prorataMonthTo +} + +export function getDifferenceInYears(from: string, to: string): number { + // Todo : take leap year into account + return getDifferenceInDays(from, to) / 365.25 +} diff --git a/source/engine/evaluation.tsx b/source/engine/evaluation.tsx index 4ef7bfc14..216a264fc 100644 --- a/source/engine/evaluation.tsx +++ b/source/engine/evaluation.tsx @@ -1,7 +1,5 @@ import { add, - any, - equals, evolve, filter, fromPairs, @@ -16,8 +14,8 @@ import { concatTemporals, liftTemporalNode, mapTemporal, - periodAverage, - pure, + pureTemporal, + temporalAverage, zipTemporals } from './period' @@ -47,14 +45,15 @@ export let evaluateNode = (cache, situationGate, parsedRules, node) => { : simplifyNodeUnit(evaluatedNode) return evaluatedNode } -const sameUnitValues = (explanation, contextRule, mecanismName) => { - const firstNodeWithUnit = explanation.find(node => !!node.unit) + +function convertNodesToSameUnit(nodes, contextRule, mecanismName) { + const firstNodeWithUnit = nodes.find(node => !!node.unit) if (!firstNodeWithUnit) { - return [undefined, explanation.map(({ nodeValue }) => nodeValue)] + return nodes } - const values = explanation.map(node => { + return nodes.map(node => { try { - return convertNodeToUnit(firstNodeWithUnit?.unit, node).nodeValue + return convertNodeToUnit(firstNodeWithUnit.unit, node) } catch (e) { typeWarning( contextRule, @@ -63,77 +62,66 @@ const sameUnitValues = (explanation, contextRule, mecanismName) => { firstNodeWithUnit?.rawNode}'`, e ) - return node.nodeValue + return node } }) - return [firstNodeWithUnit.unit, values] } -export let evaluateArray = (reducer, start) => ( +export const evaluateArray = (reducer, start) => ( cache, situationGate, parsedRules, node ) => { const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) - const temporalExplanation = concatTemporals( - node.explanation.map(evaluate).map(liftTemporalNode) + const evaluatedNodes = convertNodesToSameUnit( + node.explanation.map(evaluate), + cache._meta.contextRule, + node.name ) - const temporalEvaluations = mapTemporal(explanation => { - explanation - const [unit, values] = sameUnitValues( - explanation, - cache._meta.contextRule, - node.name - ) - const nodeValue = values.some(value => value === null) - ? null - : reduce(reducer, start, values) - const missingVariables = - node.nodeValue == null ? mergeAllMissing(explanation) : {} - return { - ...node, - nodeValue, - explanation, - missingVariables, - unit - } - }, temporalExplanation) - if (temporalEvaluations.length === 1) { - return temporalEvaluations[0] + if (!evaluatedNodes.every(Boolean)) { + console.log(node.explanation) } - const temporalValue = mapTemporal(node => node.nodeValue, temporalEvaluations) - return { + const temporalValues = concatTemporals( + evaluatedNodes.map( + ({ temporalValue, nodeValue }) => temporalValue ?? pureTemporal(nodeValue) + ) + ) + const temporalValue = mapTemporal(values => { + if (values.some(value => value === null)) { + return null + } + return reduce(reducer, start, values) + }, temporalValues) + + const baseEvaluation = { ...node, + explanation: evaluatedNodes, + unit: evaluatedNodes[0].unit + } + if (temporalValue.length === 1) { + return { + ...baseEvaluation, + nodeValue: temporalValue[0].value + } + } + return { + ...baseEvaluation, temporalValue, - nodeValue: periodAverage(temporalValue) + nodeValue: temporalAverage(temporalValue) } } -export let evaluateArrayWithFilter = (evaluationFilter, reducer, start) => ( +export const evaluateArrayWithFilter = (evaluationFilter, reducer, start) => ( cache, situationGate, parsedRules, node ) => { - let evaluateOne = child => - evaluateNode(cache, situationGate, parsedRules, child), - explanation = map( - evaluateOne, - filter(evaluationFilter(situationGate), node.explanation) - ), - [unit, values] = sameUnitValues( - explanation, - cache._meta.contextRule, - node.name - ), - nodeValue = any(equals(null), values) - ? null - : reduce(reducer, start, values), - missingVariables = - node.nodeValue == null ? mergeAllMissing(explanation) : {} - - return { ...node, nodeValue, explanation, missingVariables, unit } + return evaluateArray(reducer, start)(cache, situationGate, parsedRules, { + ...node, + explanation: filter(evaluationFilter(situationGate), node.explanation) + }) } export let defaultNode = nodeValue => ({ @@ -167,36 +155,53 @@ export let evaluateObject = (objectShape, effect) => ( Object.fromEntries, concatTemporals( Object.entries(evaluations).map(([key, node]) => - zipTemporals(pure(key), liftTemporalNode(node)) + zipTemporals(pureTemporal(key), liftTemporalNode(node)) ) ) ) - const temporalEvaluations = mapTemporal( - explanations => effect(explanations, cache, situationGate, parsedRules), - temporalExplanations - ) + const temporalExplanation = mapTemporal(explanations => { + const evaluation = effect(explanations, cache, situationGate, parsedRules) + return { + ...evaluation, + explanation: { + ...explanations, + ...evaluation.explanation + } + } + }, temporalExplanations) + + const sameUnitTemporalExplanation = convertNodesToSameUnit( + temporalExplanation.map(x => x.value), + cache._meta.contextRule, + node.name + ).map((node, i) => ({ + ...temporalExplanation[i], + value: simplifyNodeUnit(node) + })) const temporalValue = mapTemporal( - evaluation => - evaluation !== null && typeof evaluation === 'object' - ? evaluation.nodeValue - : evaluation, - temporalEvaluations + ({ nodeValue }) => nodeValue, + sameUnitTemporalExplanation ) - const nodeValue = periodAverage(temporalValue) - - return simplifyNodeUnit({ + const nodeValue = temporalAverage(temporalValue) + if (nodeValue === 495) { + console.log(temporalValue) + } + const baseEvaluation = { ...node, nodeValue, - ...(temporalEvaluations.length > 1 - ? { temporalValue } - : { - missingVariables: mergeAllMissing(Object.values(evaluations)), - explanation: { - ...evaluations, - ...temporalEvaluations[0].additionalExplanation - }, - unit: temporalEvaluations[0].unit - }) - }) + unit: sameUnitTemporalExplanation[0].value.unit, + explanation: evaluations + } + if (sameUnitTemporalExplanation.length === 1) { + return { + ...baseEvaluation, + explanation: sameUnitTemporalExplanation[0].value.explanation + } + } + return { + ...baseEvaluation, + temporalValue, + temporalExplanation + } } diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index 1e915cdcd..337e8cb31 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -15,11 +15,9 @@ import { path, pluck, reduce, - subtract, toPairs } from 'ramda' import React from 'react' -import { Trans } from 'react-i18next' import 'react-virtualized/styles.css' import { typeWarning } from './error' import { @@ -281,7 +279,7 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => { let evaluate = (currentCache, situationGate, parsedRules, node) => { let defaultRuleToEvaluate = dottedNameContext let nodeToEvaluate = recurse(node?.règle ?? defaultRuleToEvaluate) - let cache = { _meta: currentCache._meta, _metaInRecalcul: true } // Create an empty cache + let cache = { _meta: { ...currentCache._meta, inRecalcul: true } } // Create an empty cache let amendedSituation = Object.fromEntries( Object.keys(node.avec).map(dottedName => [ disambiguateRuleReference( @@ -293,7 +291,7 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => { ]) ) - if (currentCache._metaInRecalcul) { + if (currentCache._meta.inRecalcul) { return defaultNode(false) } @@ -376,7 +374,7 @@ export let mecanismReduction = (recurse, k, v) => { cache ) => { let v_assiette = assiette.nodeValue - if (v_assiette == null) return null + if (v_assiette == null) return { nodeValue: null } if (assiette.unit) { try { franchise = convertNodeToUnit(assiette.unit, franchise) @@ -431,8 +429,8 @@ export let mecanismReduction = (recurse, k, v) => { : montantFranchiséDécoté return { nodeValue, - additionalExplanation: { - unit: assiette.unit, + unit: assiette.unit, + explanation: { franchise, plafond, abattement @@ -509,9 +507,10 @@ export let mecanismProduct = (recurse, k, v) => { ) return { nodeValue, - additionalExplanation: { - plafondActif: assiette.nodeValue > plafond.nodeValue, - unit + + unit, + explanation: { + plafondActif: assiette.nodeValue > plafond.nodeValue } } } @@ -601,53 +600,6 @@ export let mecanismMin = (recurse, k, v) => { } } -export let mecanismComplement = (recurse, k, v) => { - if (v.composantes) { - //mécanisme de composantes. Voir known-mecanisms.md/composantes - return decompose(recurse, k, v) - } - - let objectShape = { cible: false, montant: false } - let effect = ({ cible, montant }) => { - let nulled = cible.nodeValue == null - return nulled - ? null - : subtract(montant.nodeValue, min(cible.nodeValue, montant.nodeValue)) - } - let explanation = parseObject(recurse, objectShape, v) - - return { - evaluate: evaluateObject(objectShape, effect), - explanation, - type: 'numeric', - category: 'mecanism', - name: 'complément pour atteindre', - // eslint-disable-next-line - jsx: (nodeValue, explanation) => ( - -
    -
  • - - cible:{' '} - - {makeJsx(explanation.cible)} -
  • -
  • - - montant à atteindre:{' '} - - {makeJsx(explanation.montant)} -
  • -
-
- ) - } -} - export let mecanismSynchronisation = (recurse, k, v) => { let evaluate = (cache, situationGate, parsedRules, node) => { let APIExplanation = evaluateNode( diff --git a/source/engine/mecanisms/régularisation.ts b/source/engine/mecanisms/régularisation.ts new file mode 100644 index 000000000..a1fda1450 --- /dev/null +++ b/source/engine/mecanisms/régularisation.ts @@ -0,0 +1,150 @@ +import { convertToString, getYear } from 'Engine/date' +import { evaluationError } from 'Engine/error' +import { evaluateNode } from 'Engine/evaluation' +import { + createTemporalEvaluation, + Evaluation, + groupByYear, + liftTemporal2, + Temporal, + temporalAverage, + temporalCumul +} from 'Engine/period' +import { Unit } from 'Engine/units' +import { DottedName } from 'Types/rule' +import { coerceArray } from '../../utils' + +export default function parse(parse, k, v) { + const rule = parse(v.règle) + if (!v['valeurs cumulées']) { + throw new Error( + 'Il manque la clé `valeurs cumulées` dans le mécanisme régularisation' + ) + } + + const variables = coerceArray(v['valeurs cumulées']).map(parse) as Array<{ + dottedName: DottedName + category: string + name: 'string' + }> + if (variables.some(({ category }) => category !== 'reference')) { + throw new Error( + 'Le mécanisme régularisation attend des noms de règles sous la clé `valeurs cumulées`' + ) + } + + return { + evaluate, + explanation: { + rule, + variables + }, + category: 'mecanism', + name: 'taux progressif', + type: 'numeric', + unit: rule.unit + } +} + +function getMonthlyCumulatedValuesOverYear( + year: number, + variable: Temporal>, + unit: Unit +): Temporal> { + const start = convertToString(new Date(year, 0, 1)) + const cumulatedPeriods = [...Array(12).keys()] + .map(monthNumber => ({ + start, + end: convertToString(new Date(year, monthNumber + 1, 0)) + })) + .map(period => { + const temporal = liftTemporal2( + (filter, value) => filter && value, + createTemporalEvaluation(true, period), + variable + ) + return { + ...period, + value: temporalCumul(temporal, unit) + } + }) + + return cumulatedPeriods +} + +function evaluate( + cache, + situation, + parsedRules, + node: ReturnType +) { + const evaluate = evaluateNode.bind(null, cache, situation, parsedRules) + + function recalculWith(situationGate: (dottedName: DottedName) => any, node) { + const newSituation = (dottedName: DottedName) => + situationGate(dottedName) ?? situation(dottedName) + return evaluateNode({ _meta: cache._meta }, newSituation, parsedRules, node) + } + + function regulariseYear(temporalEvaluation: Temporal>) { + if (temporalEvaluation.filter(({ value }) => value !== false).length <= 1) { + return temporalEvaluation + } + + const currentYear = getYear(temporalEvaluation[0].start as string) + const cumulatedVariables = node.explanation.variables.reduce( + (acc, parsedVariable) => { + const evaluation = evaluate(parsedVariable) + if (!evaluation.temporalValue) { + evaluationError( + cache._meta.contextRule, + `Dans le mécanisme régularisation, la valeur annuelle ${parsedVariable.name} n'est pas une variables temporelle` + ) + } + return { + ...acc, + [parsedVariable.dottedName]: getMonthlyCumulatedValuesOverYear( + currentYear, + evaluation.temporalValue, + evaluation.unit + ) + } + }, + {} + ) + + const cumulatedMonthlyEvaluations = [...Array(12).keys()].map(i => ({ + start: convertToString(new Date(currentYear, i, 1)), + end: convertToString(new Date(currentYear, i + 1, 0)), + value: recalculWith( + dottedName => cumulatedVariables[dottedName]?.[i].value, + node.explanation.rule + ).nodeValue + })) + const temporalRégularisée = cumulatedMonthlyEvaluations.map( + (period, i) => ({ + ...period, + value: period.value - (cumulatedMonthlyEvaluations[i - 1]?.value ?? 0) + }) + ) + + return temporalRégularisée as Temporal> + } + + const evaluation = evaluate(node.explanation.rule) + const temporalValue = evaluation.temporalValue + const evaluationWithRegularisation = groupByYear( + temporalValue as Temporal> + ) + .map(regulariseYear) + .flat() + + return { + ...node, + temporalValue: evaluationWithRegularisation, + explanation: evaluation, + nodeValue: temporalAverage(temporalValue), + missingVariables: evaluation.missingVariables, + unit: evaluation.unit + } +} diff --git a/source/engine/mecanisms/variableTemporelle.ts b/source/engine/mecanisms/variableTemporelle.ts index 90c9eb748..7b540fc3e 100644 --- a/source/engine/mecanisms/variableTemporelle.ts +++ b/source/engine/mecanisms/variableTemporelle.ts @@ -2,7 +2,7 @@ import { evaluateNode } from 'Engine/evaluation' import { createTemporalEvaluation, narrowTemporalValue, - periodAverage + temporalAverage } from 'Engine/period' import { Temporal } from './../period' @@ -19,35 +19,46 @@ function evaluate( parsedRules ) - const start = node.period.start && evaluateAttribute(node.period.start) - const end = node.period.end && evaluateAttribute(node.period.end) - const explanation = evaluateAttribute(node.explanation) + const start = + node.explanation.period.start && + evaluateAttribute(node.explanation.period.start) + const end = + node.explanation.period.end && + evaluateAttribute(node.explanation.period.end) + const value = evaluateAttribute(node.explanation.value) const period = { start: start?.nodeValue ?? null, end: end?.nodeValue ?? null } - const temporalValue = explanation.temporalValue - ? narrowTemporalValue(period, explanation.temporalValue) - : createTemporalEvaluation(explanation.nodeValue, period) + const temporalValue = value.temporalValue + ? narrowTemporalValue(period, value.temporalValue) + : createTemporalEvaluation(value.nodeValue, period) // TODO explanation missingVariables / period missing variables return { ...node, - nodeValue: periodAverage(temporalValue as Temporal), + nodeValue: temporalAverage(temporalValue as Temporal, value.unit), temporalValue, - period: { start, end }, - explanation + explanation: { + period: { start, end }, + value + }, + unit: value.unit } } export default function parseVariableTemporelle(parse, __, v: any) { + const explanation = parse(v.explanation) return { evaluate, - explanation: parse(v.explanation), - period: { - start: v.period.start && parse(v.period.start), - end: v.period.end && parse(v.period.end) - } + explanation: { + period: { + start: v.period.start && parse(v.period.start), + end: v.period.end && parse(v.period.end) + }, + value: explanation + }, + unit: explanation.unit } } diff --git a/source/engine/parse.tsx b/source/engine/parse.tsx index e82875715..0fe14112d 100644 --- a/source/engine/parse.tsx +++ b/source/engine/parse.tsx @@ -9,6 +9,7 @@ import durée from 'Engine/mecanisms/durée' import encadrement from 'Engine/mecanisms/encadrement' import grille from 'Engine/mecanisms/grille' import operation from 'Engine/mecanisms/operation' +import régularisation from 'Engine/mecanisms/régularisation' import tauxProgressif from 'Engine/mecanisms/tauxProgressif' import variableTemporelle from 'Engine/mecanisms/variableTemporelle' import variations from 'Engine/mecanisms/variations' @@ -183,6 +184,7 @@ const statelessParseFunction = { 'une de ces conditions': mecanismOneOf, 'toutes ces conditions': mecanismAllOf, somme: mecanismSum, + régularisation, multiplication: mecanismProduct, produit: mecanismProduct, temporalValue: variableTemporelle, diff --git a/source/engine/period.ts b/source/engine/period.ts index 0a6771add..4b2135d9c 100644 --- a/source/engine/period.ts +++ b/source/engine/period.ts @@ -1,8 +1,16 @@ -import { convertToDate, getRelativeDate } from 'Engine/date' +import { + convertToDate, + getDifferenceInDays, + getDifferenceInMonths, + getDifferenceInYears, + getRelativeDate, + getYear +} from 'Engine/date' +import { Unit } from './units' -export type Period = { - start: Date | null - end: Date | null +export type Period = { + start: T | null + end: T | null } export function parsePeriod(word: string, date: Date): Period { @@ -38,7 +46,6 @@ export function parsePeriod(word: string, date: Date): Period { } } if (word === 'en') { - console.log(word, date) return { start: null, end: null } } if (startWords.includes(word)) { @@ -58,14 +65,14 @@ export function parsePeriod(word: string, date: Date): Period { // 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)] -type Evaluation = T | false | null +export type Evaluation = T | false | null -type EvaluatedNode = { +export type EvaluatedNode = { nodeValue: Evaluation temporalValue?: Temporal> } -type TemporalNode = Temporal<{ nodeValue: Evaluation }> +export type TemporalNode = Temporal<{ nodeValue: Evaluation }> export type Temporal = Array & { value: T }> export function narrowTemporalValue( @@ -102,7 +109,7 @@ export function createTemporalEvaluation( return temporalValue } -export function pure(value: T): Temporal { +export function pureTemporal(value: T): Temporal { return [{ start: null, end: null, value }] } @@ -117,7 +124,7 @@ export function mapTemporal( })) } -function liftTemporal2( +export function liftTemporal2( fn: (value1: T1, value2: T2) => T3, temporalValue1: Temporal, temporalValue2: Temporal @@ -133,14 +140,14 @@ export function concatTemporals( ): Temporal> { return temporalValues.reduce( (values, value) => liftTemporal2((a, b) => [...a, b], values, value), - pure([]) as Temporal> + pureTemporal([]) as Temporal> ) } export function liftTemporalNode(node: EvaluatedNode): TemporalNode { const { temporalValue, ...baseNode } = node if (!temporalValue) { - return pure(baseNode) + return pureTemporal(baseNode) } return mapTemporal( nodeValue => ({ @@ -211,6 +218,84 @@ export function zipTemporals( throw new EvalError('All case should have been covered') } +function beginningOfNextYear(date: string): string { + return `01/01/${getYear(date) + 1}` +} + +function endsOfPreviousYear(date: string): string { + return `31/12/${getYear(date) - 1}` +} + +function splitStartsAt( + fn: (date: string) => string, + temporal: Temporal +): Temporal { + return temporal.reduce((acc, period) => { + const { start, end } = period + const newStart = start === null ? start : fn(start) + if (compareEndDate(newStart, end) !== -1) { + return [...acc, period] + } + console.assert(newStart !== null) + return [ + ...acc, + { ...period, end: getRelativeDate(newStart as string, -1) }, + { ...period, start: newStart } + ] + }, [] as Temporal) +} + +function splitEndsAt( + fn: (date: string) => string, + temporal: Temporal +): Temporal { + return temporal.reduce((acc, period) => { + const { start, end } = period + const newEnd = end === null ? end : fn(end) + if (compareStartDate(start, newEnd) !== -1) { + return [...acc, period] + } + console.assert(newEnd !== null) + return [ + ...acc, + { ...period, end: newEnd }, + { ...period, start: getRelativeDate(newEnd as string, 1) } + ] + }, [] as Temporal) +} + +export function groupByYear(temporalValue: Temporal): Array> { + return ( + // First step: split period by year if needed + splitEndsAt( + endsOfPreviousYear, + splitStartsAt(beginningOfNextYear, temporalValue) + ) + // Second step: group period by year + .reduce((acc, period) => { + const [currentTemporal, ...otherTemporal] = acc + if (currentTemporal === undefined) { + return [[period]] + } + const firstPeriod = currentTemporal[0] + console.assert( + firstPeriod !== undefined && + firstPeriod.end !== null && + period.start !== null, + 'invariant non verifié' + ) + if ( + (firstPeriod.end as string).slice(-4) !== + (period.start as string).slice(-4) + ) { + return [[period], ...acc] + } + return [[...currentTemporal, period], ...otherTemporal] + }, [] as Array>) + .reverse() + ) +} + function simplify(temporalValue: Temporal): Temporal { return temporalValue } @@ -247,8 +332,9 @@ function compareEndDate( return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1 } -export function periodAverage( - temporalValue: Temporal> +export function temporalAverage( + temporalValue: Temporal>, + unit?: Unit ): Evaluation { temporalValue = temporalValue.filter(({ value }) => value !== false) const first = temporalValue[0] @@ -265,7 +351,7 @@ export function periodAverage( if (last.end != null) { return first.value } - return first.value + last.value / 2 + return (first.value + last.value) / 2 } if (temporalValue.some(({ value }) => value == null)) { @@ -273,11 +359,14 @@ export function periodAverage( } let totalWeight = 0 const weights = temporalValue.map(({ start, end, value }) => { - const day = 1000 * 60 * 60 * 24 - const weight = - convertToDate(end as string).getTime() - - convertToDate(start as string).getTime() + - day + let weight = 0 + if (unit?.denominators.includes('mois')) { + weight = getDifferenceInMonths(start, end) + } else if (unit?.denominators.includes('année')) { + weight = getDifferenceInYears(start, end) + } else { + weight = getDifferenceInDays(start, end) + } totalWeight += weight return (value as number) * weight }) @@ -286,3 +375,41 @@ export function periodAverage( 0 ) } + +export function temporalCumul( + temporalValue: Temporal>, + unit: Unit +): Evaluation { + temporalValue = temporalValue.filter(({ value }) => value !== false) + const first = temporalValue[0] + const last = temporalValue[temporalValue.length - 1] + if (!temporalValue.length) { + return false + } + + // La variable est définie sur un interval infini + if (first.start == null || last.end == null) { + if (first.start != null) { + return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity + } + if (last.end != null) { + return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity + } + return null + } + if (temporalValue.some(({ value }) => value == null)) { + return null + } + + return temporalValue.reduce((acc, { start, end, value }) => { + let weight = 1 + if (unit?.denominators.includes('mois')) { + weight = getDifferenceInMonths(start, end) + } else if (unit?.denominators.includes('année')) { + weight = getDifferenceInYears(start, end) + } else if (unit?.denominators.includes('jour')) { + weight = getDifferenceInDays(start, end) + } + return value * weight + acc + }, 0) +} diff --git a/test/date.test.js b/test/date.test.js new file mode 100644 index 000000000..b8c6779ee --- /dev/null +++ b/test/date.test.js @@ -0,0 +1,19 @@ +import { expect } from 'chai' +import { getDifferenceInMonths } from '../source/engine/date' + +describe('Date : getDifferenceInMonths', () => { + it('should compute the difference for one full month', () => { + expect(getDifferenceInMonths('01/01/2020', '31/01/2020')).to.equal(1) + }) + it('should compute the difference for one month and one day', () => { + expect(getDifferenceInMonths('01/01/2020', '01/02/2020')).to.equal( + 1 + 1 / 29 + ) + }) + it('should compute the difference for 2 days between months', () => { + expect(getDifferenceInMonths('31/01/2020', '01/02/2020')).to.approximately( + 1 / 31 + 1 / 29, + 0.000000000001 + ) + }) +}) diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml new file mode 100644 index 000000000..afb32a89e --- /dev/null +++ b/test/mécanismes/régularisation.yaml @@ -0,0 +1,40 @@ +salaire: + unité: €/mois + formule: + somme: + - 3300 €/mois | du 01/01/2020 | au 29/02/2020 + - 3600 €/mois | du 01/03/2020 | au 31/12/2020 + +plafond sécurité sociale: + unité: €/mois + formule: 3500 €/mois | du 01/01/2020 | au 31/12/2020 + +retraite: + formule: + multiplication: + assiette: salaire + plafond: plafond sécurité sociale + taux: 10% + +retraite . avec régularisation: + formule: + régularisation: + règle: retraite + valeurs cumulées: + - salaire + - plafond sécurité sociale + +régularisation . avant passage: + formule: retraite . avec régularisation | du 01/01/2020 | au 29/02/2020 + exemples: + - valeur attendue: 330 + +régularisation . test mois régularisés: + formule: retraite . avec régularisation | du 01/03/2020 | au 30/06/2020 + exemples: + - valeur attendue: 360 + +régularisation . test mois après régularisation: + formule: retraite . avec régularisation | du 01/07/2020 | au 31/12/2020 + exemples: + - valeur attendue: 350 diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml index 6e5d6bc69..85672060b 100644 --- a/test/mécanismes/variable-temporelle.yaml +++ b/test/mécanismes/variable-temporelle.yaml @@ -164,15 +164,15 @@ variable temporelle numérique . multiplication avec valeur au dessus du plafond exemples: - valeur attendue: 337.7 # (2000 * 8 + 2200 * 23)/31 -# variable temporelle numérique . multiplication avec valeur sur l'année: -# formule: contrat salarié . cotisations . retraite | du 01/01/2019 | au 31/12/2019 -# exemples: -# # 200 * 6 [janvier-juin] -# # + 214.839 [juillet] -# # + 220 * 4 [aout-novembre] -# # + 337.7 [décembre] -# # /12 mois -# - valeur attendue: 219.378 +variable temporelle numérique . multiplication avec valeur sur l'année: + formule: contrat salarié . cotisations . retraite | du 01/01/2019 | au 31/12/2019 + exemples: + # 200 * 6 [janvier-juin] + # + 214.839 [juillet] + # + 220 * 4 [aout-novembre] + # + 337.7 [décembre] + # /12 mois + - valeur attendue: 219.378 # test . proratisation du salaire avec entrée en cours de mois: # formule: salaire brut [avril 2019] # exemples: diff --git a/test/period.test.js b/test/period.test.js index 014231d30..416c82aec 100644 --- a/test/period.test.js +++ b/test/period.test.js @@ -2,6 +2,7 @@ import { expect } from 'chai' import { concatTemporals, createTemporalEvaluation, + groupByYear, zipTemporals } from '../source/engine/period' @@ -69,3 +70,63 @@ describe('Periods : concat', () => { ]) }) }) + +describe('Periods : groupByYear', () => { + const invariants = temporalYear => { + const startDate = temporalYear[0].start + const endDate = temporalYear.slice(-1)[0].end + expect( + startDate === null || startDate.startsWith('01/01'), + 'starts at the beginning of a year' + ) + expect( + endDate === null || endDate.startsWith('31/12'), + 'stops at the end of a year' + ) + } + it('should handle constant value', () => { + const value = createTemporalEvaluation(10) + expect(groupByYear(value)).to.deep.equal([value]) + }) + it('should handle changing value', () => { + const value = createTemporalEvaluation(10, { + start: '06/06/2020', + end: '20/12/2020' + }) + const result = groupByYear(value) + expect(result).to.have.length(3) + result.forEach(invariants) + }) + it('should handle changing value over several years', () => { + const value = createTemporalEvaluation(10, { + start: '06/06/2020', + end: '20/12/2022' + }) + const result = groupByYear(value) + expect(result).to.have.length(5) + result.forEach(invariants) + }) + it('should handle complex case', () => { + const result = groupByYear( + concatTemporals([ + createTemporalEvaluation(1, { + start: '06/06/2020', + end: '20/12/2022' + }), + createTemporalEvaluation(2, { + start: '01/01/1991', + end: '20/12/1992' + }), + createTemporalEvaluation(3, { + start: '31/01/1990', + end: '20/12/2021' + }), + createTemporalEvaluation(4, { + start: '31/12/2020', + end: '01/01/2021' + }) + ]) + ) + result.forEach(invariants) + }) +}) From c0ad3c8a6ee21df4fdcfddefe4b2bc5f1a84a481 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Thu, 27 Feb 2020 19:53:27 +0100 Subject: [PATCH 07/13] :gear: ajoute le calcul des variables temporelles dans les expressions --- source/engine/date.ts | 15 ---- source/engine/mecanisms/operation.js | 48 ++++++----- source/engine/mecanisms/variableTemporelle.ts | 1 - source/engine/nodeUnits.ts | 8 ++ test/mécanismes/régularisation.yaml | 9 ++- test/mécanismes/variable-temporelle.yaml | 80 +++++-------------- 6 files changed, 63 insertions(+), 98 deletions(-) diff --git a/source/engine/date.ts b/source/engine/date.ts index 04bf8b824..3c4273321 100644 --- a/source/engine/date.ts +++ b/source/engine/date.ts @@ -18,7 +18,6 @@ export function normalizeDate( return `${pad(day)}/${pad(month)}/${pad(year)}` } -const dateRegexp = /[\d]{2}\/[\d]{2}\/[\d]{4}/ export function convertToDate(value: string): Date { const [day, month, year] = normalizeDateString(value).split('/') var result = new Date(+year, +month - 1, +day) @@ -27,20 +26,6 @@ export function convertToDate(value: string): Date { result.setMinutes(result.getMinutes() - result.getTimezoneOffset()) return result } -export function convertToDateIfNeeded(...values: string[]) { - const dateStrings = values.map(dateString => '' + dateString) - if (!dateStrings.some(dateString => dateString.match(dateRegexp))) { - return values - } - dateStrings.forEach(dateString => { - if (!dateString.match(dateRegexp)) { - throw new TypeError( - `'${dateString}' n'est pas une date valide (format attendu: mm/aaaa ou jj/mm/aaaa)` - ) - } - }) - return dateStrings.map(convertToDate) -} export function convertToString(date: Date): string { return normalizeDate(date.getFullYear(), date.getMonth() + 1, date.getDate()) diff --git a/source/engine/mecanisms/operation.js b/source/engine/mecanisms/operation.js index 8587e2806..fdac979ee 100644 --- a/source/engine/mecanisms/operation.js +++ b/source/engine/mecanisms/operation.js @@ -2,10 +2,10 @@ import { typeWarning } from 'Engine/error' import { evaluateNode, makeJsx, mergeMissing } from 'Engine/evaluation' import { Node } from 'Engine/mecanismViews/common' import { convertNodeToUnit } from 'Engine/nodeUnits' +import { liftTemporal2, pureTemporal, temporalAverage } from 'Engine/period' import { inferUnit, serializeUnit } from 'Engine/units' import { curry, map } from 'ramda' import React from 'react' -import { convertToDateIfNeeded } from '../date.ts' export default (k, operatorFunction, symbol) => (recurse, k, v) => { let evaluate = (cache, situation, parsedRules, node) => { @@ -43,30 +43,36 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => { ) } } - let node1Value = node1.nodeValue - let node2Value = node2.nodeValue - try { - ;[node1Value, node2Value] = convertToDateIfNeeded( - node1.nodeValue, - node2.nodeValue - ) - } catch (e) { - typeWarning( - cache._meta.contextRule, - `Impossible de convertir une des valeur en date`, - e - ) - } - let nodeValue = operatorFunction(node1Value, node2Value) - - let unit = inferUnit(k, [node1.unit, node2.unit]) - return { + const baseNode = { ...node, - nodeValue, - unit, explanation, + unit: inferUnit(k, [node1.unit, node2.unit]), missingVariables } + + let temporalValue = liftTemporal2( + (a, b) => { + if (a === false && ['∕', '-'].includes(node.operator)) { + return false + } + if (a === false) { + return b + } + if (b === false) { + return a + } + return operatorFunction(a, b) + }, + node1.temporalValue ?? pureTemporal(node1.nodeValue), + node2.temporalValue ?? pureTemporal(node2.nodeValue) + ) + const nodeValue = temporalAverage(temporalValue, baseNode.unit) + + return { + ...baseNode, + nodeValue, + ...(temporalValue.length > 1 && { temporalValue }) + } } let explanation = v.explanation.map(recurse) diff --git a/source/engine/mecanisms/variableTemporelle.ts b/source/engine/mecanisms/variableTemporelle.ts index 7b540fc3e..194d3b943 100644 --- a/source/engine/mecanisms/variableTemporelle.ts +++ b/source/engine/mecanisms/variableTemporelle.ts @@ -35,7 +35,6 @@ function evaluate( ? narrowTemporalValue(period, value.temporalValue) : createTemporalEvaluation(value.nodeValue, period) // TODO explanation missingVariables / period missing variables - return { ...node, nodeValue: temporalAverage(temporalValue as Temporal, value.unit), diff --git a/source/engine/nodeUnits.ts b/source/engine/nodeUnits.ts index c0ef9c773..d8e24ad09 100644 --- a/source/engine/nodeUnits.ts +++ b/source/engine/nodeUnits.ts @@ -1,3 +1,4 @@ +import { mapTemporal } from './period' import { areUnitConvertible, convertUnit, @@ -41,6 +42,13 @@ export function convertNodeToUnit(to: Unit, node) { nodeValue: node.unit ? convertUnit(node.unit, to, node.nodeValue) : node.nodeValue, + temporalValue: + node.temporalValue && node.unit + ? mapTemporal( + value => convertUnit(node.unit, to, value), + node.temporalValue + ) + : node.temporalValue, unit: to } } diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml index afb32a89e..7416f9a11 100644 --- a/test/mécanismes/régularisation.yaml +++ b/test/mécanismes/régularisation.yaml @@ -19,8 +19,13 @@ retraite: retraite . avec régularisation: formule: régularisation: - règle: retraite - valeurs cumulées: + règle: + multiplication: + assiette: salaire + plafond: plafond sécurité sociale + taux: 10% + + valeurs annualisée: - salaire - plafond sécurité sociale diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml index 85672060b..d66063a8d 100644 --- a/test/mécanismes/variable-temporelle.yaml +++ b/test/mécanismes/variable-temporelle.yaml @@ -74,7 +74,7 @@ variable temporelle numérique . variable . test date applicable 2: - valeur attendue: 40 variable temporelle numérique . addition . valeur: - formule: (20 €/mois | à partir du 15/11/2019) + (10 €/mois | à partir du 12/09/2020) + formule: (20 €/mois | à partir du 15/11/2019) + (10 €/mois | à partir du 01/02/2020) date: variable temporelle numérique . addition . test date: @@ -89,25 +89,23 @@ variable temporelle numérique . addition . test date: - situation: date: 12/09/2020 valeur attendue: 30 - -# variable temporelle numérique . somme . valeur 2: -# formule: valeur - (120 €/an | jusqu'au 04/03/2020) -# variable temporelle numérique . test date non applicable: -# formule: valeur -# exemples: -# - valeur attendue: non - -# variable temporelle numérique . test date applicable: -# formule: valeur [le 12/02/2020] -# exemples: -# - valeur attendue: 40 - -# variable temporelle numérique . test date applicable avec unité: -# formule: valeur [en 2019, en 2020, €/mois] -# unité: €/mois -# exemples: -# - valeur attendue: 40 - +début: +fin: +variable temporelle numérique . expression: + formule: "(addition . valeur * (50% | du 01/01/2020 | au 31/01/2020)) | depuis : début | jusqu'à : fin" + exemples: + - situation: + début: 01/01/2020 + fin: 31/01/2020 + valeur attendue: 10 + - situation: + début: 01/01/2020 + fin: 29/02/2020 + valeur attendue: 20 + - situation: + début: 01/02/2020 + fin: 31/03/2020 + valeur attendue: 30 contrat salarié . date d'embauche: formule: 12/09/2018 @@ -167,49 +165,13 @@ variable temporelle numérique . multiplication avec valeur au dessus du plafond variable temporelle numérique . multiplication avec valeur sur l'année: formule: contrat salarié . cotisations . retraite | du 01/01/2019 | au 31/12/2019 exemples: - # 200 * 6 [janvier-juin] + # 200 * 7 [janvier-juin] # + 214.839 [juillet] - # + 220 * 4 [aout-novembre] + # + 220 * 3 [aout-novembre] # + 337.7 [décembre] # /12 mois - - valeur attendue: 219.378 + - valeur attendue: 217.7115 # test . proratisation du salaire avec entrée en cours de mois: # formule: salaire brut [avril 2019] # exemples: # - valeur attendue: 400 # (2000 * 6)/30 - -# test . proratisation du salaire avec augmentation en cours de mois: -# formule: salaire brut [juillet 2019] -# exemples: -# - valeur attendue: 2148.39 # (2000 * 8 + 2200 * 23)/31 - -# variable temporelle numérique . test proratisation du PSS pour le mois d'embauche: -# formule: contrat salarié . PSS proratisé | du 01/04/2019 | au 30/04/2019 -# exemples: -# - valeur attendue: 1688.5 -# cotisations . tranche 1 . montant : -# formule: -# PSS - salaire brut - -# cotisations . tranche 1 . régularisation : -# formule: -# '(PSS - salaire brut) | depuis le 01/01/2020' - -# cotisations . retraite employeur plafonnée: -# formule: -# multiplication: -# taux: 8.55% -# assiette: salaire brut -# plafond: PSS proratisé -# régularisation: progressive sur l'année civile - -# test . régularisation: -# description: >- -# Bien que le salaire du mois de décembre soit supérieur au -# plafond, le taux effectif reste 8.55% du fait de la -# régularisation des mois précédents -# période: -# en: décembre 2019 -# formule: retraite employeur plafonnée -# exemples: -# - valeur attendue: 359.1 #4200 * 8.55% From a9add94f83daa521dab23f6450a429e655428c37 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Fri, 28 Feb 2020 15:03:41 +0100 Subject: [PATCH 08/13] :gear: ajoute les variables temporelles pour la variation --- source/engine/mecanisms/operation.js | 21 +++- source/engine/mecanisms/variations.js | 117 ----------------- source/engine/mecanisms/variations.ts | 152 +++++++++++++++++++++++ source/engine/period.ts | 9 ++ test/mécanismes/régularisation.yaml | 2 +- test/mécanismes/variable-temporelle.yaml | 46 ++++++- 6 files changed, 221 insertions(+), 126 deletions(-) delete mode 100644 source/engine/mecanisms/variations.js create mode 100644 source/engine/mecanisms/variations.ts diff --git a/source/engine/mecanisms/operation.js b/source/engine/mecanisms/operation.js index fdac979ee..3bfed3083 100644 --- a/source/engine/mecanisms/operation.js +++ b/source/engine/mecanisms/operation.js @@ -1,3 +1,4 @@ +import { convertToDate } from 'Engine/date' import { typeWarning } from 'Engine/error' import { evaluateNode, makeJsx, mergeMissing } from 'Engine/evaluation' import { Node } from 'Engine/mecanismViews/common' @@ -7,6 +8,7 @@ import { inferUnit, serializeUnit } from 'Engine/units' import { curry, map } from 'ramda' import React from 'react' +const comparisonOperator = ['≠', '=', '<', '>', '≤', '≥'] export default (k, operatorFunction, symbol) => (recurse, k, v) => { let evaluate = (cache, situation, parsedRules, node) => { const explanation = map( @@ -52,20 +54,33 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => { let temporalValue = liftTemporal2( (a, b) => { - if (a === false && ['∕', '-'].includes(node.operator)) { + if (['∕', '-'].includes(node.operator) && a === false) { return false } - if (a === false) { + if (['×', '+'].includes(node.operator) && a === false) { return b } - if (b === false) { + if (['∕', '-', '×', '+'].includes(node.operator) && b === false) { return a } + if ( + !['=', '≠'].includes(node.operator) && + (a === false || b === false) + ) { + return false + } + if ( + comparisonOperator.includes(node.operator) && + [a, b].every(value => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/)) + ) { + return operatorFunction(convertToDate(a), convertToDate(b)) + } return operatorFunction(a, b) }, node1.temporalValue ?? pureTemporal(node1.nodeValue), node2.temporalValue ?? pureTemporal(node2.nodeValue) ) + const nodeValue = temporalAverage(temporalValue, baseNode.unit) return { diff --git a/source/engine/mecanisms/variations.js b/source/engine/mecanisms/variations.js deleted file mode 100644 index 127e28ac3..000000000 --- a/source/engine/mecanisms/variations.js +++ /dev/null @@ -1,117 +0,0 @@ -import { - bonus, - collectNodeMissing, - evaluateNode, - mergeAllMissing, - mergeMissing -} from 'Engine/evaluation' -import Variations from 'Engine/mecanismViews/Variations' -import { inferUnit } from 'Engine/units' -import { dissoc, filter, isNil, pluck, reduce, reject } from 'ramda' - -/* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */ -export default (recurse, k, v, devariate) => { - let explanation = devariate - ? devariateExplanation(recurse, k, v) - : v.map(({ si, alors, sinon }) => - sinon !== undefined - ? { consequence: recurse(sinon), condition: undefined } - : { consequence: recurse(alors), condition: recurse(si) } - ) - - let evaluate = (cache, situationGate, parsedRules, node) => { - let evaluateVariationProp = prop => - prop && evaluateNode(cache, situationGate, parsedRules, prop), - // mark the satisfied variation if any in the explanation - [, resolvedExplanation] = reduce( - ([resolved, result], variation) => { - if (resolved) return [true, [...result, variation]] - - // evaluate the condition - let evaluatedCondition = evaluateVariationProp(variation.condition) - - if (evaluatedCondition == undefined) { - // No condition : we've reached the eventual defaut case - let evaluatedVariation = { - consequence: evaluateVariationProp(variation.consequence), - satisfied: true - } - return [true, [...result, evaluatedVariation]] - } - - if (evaluatedCondition.nodeValue === null) - // the current variation case has missing variables => we can't go further - return [ - true, - [...result, { ...variation, condition: evaluatedCondition }] - ] - - if (evaluatedCondition.nodeValue === true) { - let evaluatedVariation = { - condition: evaluatedCondition, - consequence: evaluateVariationProp(variation.consequence), - satisfied: true - } - return [true, [...result, evaluatedVariation]] - } - return [false, [...result, variation]] - }, - [false, []] - )(node.explanation), - satisfiedVariation = resolvedExplanation.find(v => v.satisfied), - nodeValue = satisfiedVariation - ? satisfiedVariation.consequence.nodeValue - : null - - let leftMissing = mergeAllMissing( - reject(isNil, pluck('condition', resolvedExplanation)) - ), - candidateVariations = filter( - node => !node.condition || node.condition.nodeValue !== false, - resolvedExplanation - ), - rightMissing = mergeAllMissing( - reject(isNil, pluck('consequence', candidateVariations)) - ), - missingVariables = satisfiedVariation - ? collectNodeMissing(satisfiedVariation.consequence) - : mergeMissing(bonus(leftMissing), rightMissing) - - return { - ...node, - nodeValue, - ...(satisfiedVariation && { unit: satisfiedVariation?.consequence.unit }), - explanation: resolvedExplanation, - missingVariables - } - } - - // TODO - find an appropriate representation - return { - explanation, - evaluate, - jsx: Variations, - category: 'mecanism', - name: 'variations', - type: 'numeric', - unit: inferUnit( - '+', - explanation.map(r => r.consequence.unit) - ) - } -} - -export let devariateExplanation = (recurse, mecanismKey, v) => { - let fixedProps = dissoc('variations')(v), - explanation = v.variations.map(({ si, alors, sinon }) => ({ - consequence: recurse({ - [mecanismKey]: { - ...fixedProps, - ...(sinon || alors) - } - }), - condition: sinon ? undefined : recurse(si) - })) - - return explanation -} diff --git a/source/engine/mecanisms/variations.ts b/source/engine/mecanisms/variations.ts new file mode 100644 index 000000000..69659af98 --- /dev/null +++ b/source/engine/mecanisms/variations.ts @@ -0,0 +1,152 @@ +import { typeWarning } from 'Engine/error' +import { defaultNode, evaluateNode } from 'Engine/evaluation' +import Variations from 'Engine/mecanismViews/Variations' +import { convertNodeToUnit } from 'Engine/nodeUnits' +import { + liftTemporal2, + mapTemporal, + pureTemporal, + sometime, + temporalAverage +} from 'Engine/period' +import { inferUnit } from 'Engine/units' +import { and, dissoc, not, or } from 'ramda' +import { mergeAllMissing } from './../evaluation' + +/* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */ +export default function parse(recurse, k, v, devariate) { + let explanation = devariate + ? devariateExplanation(recurse, k, v) + : v.map(({ si, alors, sinon }) => + sinon !== undefined + ? { consequence: recurse(sinon), condition: defaultNode(true) } + : { consequence: recurse(alors), condition: recurse(si) } + ) + + // TODO - find an appropriate representation + return { + explanation, + evaluate, + jsx: Variations, + category: 'mecanism', + name: 'variations', + type: 'numeric', + unit: inferUnit( + '+', + explanation.map(r => r.consequence.unit) + ) + } +} + +export let devariateExplanation = (recurse, mecanismKey, v) => { + let fixedProps = dissoc('variations')(v), + explanation = v.variations.map(({ si, alors, sinon }) => ({ + consequence: recurse({ + [mecanismKey]: { + ...fixedProps, + ...(sinon || alors) + } + }), + condition: sinon ? defaultNode(true) : recurse(si) + })) + + return explanation +} + +function evaluate( + cache, + situationGate, + parsedRules, + node: ReturnType +) { + const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) + + const [temporalValue, explanation, unit] = node.explanation.reduce( + ( + [evaluation, explanations, unit, previousConditions], + { condition, consequence }, + i: number + ) => { + const previousConditionsAlwaysTrue = !sometime( + value => value !== true, + previousConditions + ) + if (previousConditionsAlwaysTrue) { + return [ + evaluation, + [...explanations, { condition, consequence }], + unit, + previousConditions + ] + } + const evaluatedCondition = evaluate(condition) + const currentCondition = liftTemporal2( + and, + mapTemporal(not, previousConditions), + evaluatedCondition.temporalValue ?? + pureTemporal(evaluatedCondition.nodeValue) + ) + const currentConditionAlwaysFalse = !sometime(Boolean, currentCondition) + if (currentConditionAlwaysFalse) { + return [ + evaluation, + [...explanations, { condition: evaluatedCondition, consequence }], + unit, + previousConditions + ] + } + let evaluatedConsequence = evaluate(consequence) + + try { + evaluatedConsequence = convertNodeToUnit(unit, evaluatedConsequence) + } catch (e) { + return typeWarning( + cache._meta.contexRule, + `L'unité de la branche n° ${i} du mécanisme 'variations' n'est pas compatible avec celle d'une branche précédente`, + e + ) + } + const currentValue = liftTemporal2( + (cond, value) => cond && value, + currentCondition, + evaluatedConsequence.temporalValue ?? + pureTemporal(evaluatedConsequence.nodeValue) + ) + + return [ + liftTemporal2(or, evaluation, currentValue), + [ + ...explanations, + { + condition: evaluatedCondition, + consequence: evaluatedConsequence + } + ], + unit || evaluatedConsequence.unit, + liftTemporal2(or, previousConditions, currentCondition) + ] + }, + [pureTemporal(false), [], node.unit, pureTemporal(false)] + ) + const nodeValue = temporalAverage(temporalValue, unit) + const missingVariables = mergeAllMissing( + explanation.reduce( + (values, { condition, consequence }) => [ + ...values, + condition, + consequence + ], + [] + ) + ) + return { + ...node, + nodeValue, + unit, + explanation, + missingVariables, + ...(temporalValue.length > 1 && { temporalValue }) + // TODO + // missingVariables + } +} diff --git a/source/engine/period.ts b/source/engine/period.ts index 4b2135d9c..cee75af2e 100644 --- a/source/engine/period.ts +++ b/source/engine/period.ts @@ -123,6 +123,12 @@ export function mapTemporal( value: fn(value) })) } +export function sometime( + fn: (value: T1) => boolean, + temporalValue: Temporal +): boolean { + return temporalValue.some(({ start, end, value }) => fn(value)) +} export function liftTemporal2( fn: (value1: T1, value2: T2) => T3, @@ -342,6 +348,9 @@ export function temporalAverage( if (!temporalValue.length) { return false } + if (temporalValue.length === 1) { + return temporalValue[0].value + } // La variable est définie sur un interval infini if (first.start == null || last.end == null) { diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml index 7416f9a11..5dd8ba78c 100644 --- a/test/mécanismes/régularisation.yaml +++ b/test/mécanismes/régularisation.yaml @@ -25,7 +25,7 @@ retraite . avec régularisation: plafond: plafond sécurité sociale taux: 10% - valeurs annualisée: + valeurs cumulées: - salaire - plafond sécurité sociale diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml index d66063a8d..80fce38c4 100644 --- a/test/mécanismes/variable-temporelle.yaml +++ b/test/mécanismes/variable-temporelle.yaml @@ -73,12 +73,12 @@ variable temporelle numérique . variable . test date applicable 2: exemples: - valeur attendue: 40 -variable temporelle numérique . addition . valeur: +prix: formule: (20 €/mois | à partir du 15/11/2019) + (10 €/mois | à partir du 01/02/2020) date: -variable temporelle numérique . addition . test date: - formule: 'valeur | le : date' +variable temporelle numérique . test addition: + formule: 'prix | le : date' exemples: - situation: date: 01/01/2019 @@ -89,10 +89,15 @@ variable temporelle numérique . addition . test date: - situation: date: 12/09/2020 valeur attendue: 30 + +prix avec variations: + formule: prix * (50% | du 01/01/2020 | au 31/01/2020) + début: fin: -variable temporelle numérique . expression: - formule: "(addition . valeur * (50% | du 01/01/2020 | au 31/01/2020)) | depuis : début | jusqu'à : fin" +variable temporelle numérique . expression . multiplication: + formule: "prix avec variations | depuis : début | jusqu'à : fin" + # 20 [avant janvier] / 10 [pendant janvier] | 30 [pendant et après février] exemples: - situation: début: 01/01/2020 @@ -106,6 +111,37 @@ variable temporelle numérique . expression: début: 01/02/2020 fin: 31/03/2020 valeur attendue: 30 + +taux associé: + formule: + variations: + - si: prix avec variations >= 20 €/mois + alors: 10%/mois + - si: prix avec variations < 20 €/mois + alors: 60%/mois + # 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: + formule: "taux associé | depuis : début | jusqu'à : fin" + exemples: + - situation: + début: 01/01/2020 + fin: 31/01/2020 + valeur attendue: 60 + - situation: + début: 01/01/2020 + fin: 29/02/2020 + valeur attendue: 35 + - situation: + début: 01/02/2020 + fin: 31/03/2020 + valeur attendue: 10 + - situation: + début: 01/10/2019 + fin: 30/10/2019 + valeur attendue: 5 + contrat salarié . date d'embauche: formule: 12/09/2018 From c3e26bed33c8464d8438812f794f9fb30a8ba9c3 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Fri, 28 Feb 2020 15:54:39 +0100 Subject: [PATCH 09/13] =?UTF-8?q?:wip:=20exemple=20plus=20complexe=20pour?= =?UTF-8?q?=20la=20r=C3=A9gul?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/mécanismes/régularisation.yaml | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml index 5dd8ba78c..25396bad4 100644 --- a/test/mécanismes/régularisation.yaml +++ b/test/mécanismes/régularisation.yaml @@ -43,3 +43,54 @@ régularisation . test mois après régularisation: formule: retraite . avec régularisation | du 01/07/2020 | au 31/12/2020 exemples: - valeur attendue: 350 + +# ====================== +# Exemple plus complexe +# ====================== + +heures d'absences: + # TODO : mettre les heures chaque jour + formule: + somme: + - 2 heures/mois | du 01/01/2020 | au 31/01/2020 + - 3 heures/mois | du 01/03/2020 | au 31/03/2020 + +temps contractuel: + formule: 145 heures/mois + +temps de travail effectif: + formule: temps contractuel - heures d'absences + +plafond sécurité sociale proratisé: + formule: + multiplication: + assiette: plafond sécurité sociale + facteur: temps de travail effectif / 151.67 heures/mois + +taux variable: + formule: + variations: + - si: salaire < plafond sécurité sociale proratisé + alors: 10% + - sinon: 20% + +cotisation spéciale: + formule: + régularisation: + règle: + multiplication: + assiette: salaire + taux: taux variable + valeurs cumulées: + - salaire + - plafond sécurité sociale proratisé + +régularisation . test variations 1: + formule: cotisation spéciale | du 01/01/2020 | au 31/12/2020 + exemples: + - valeur attendue: 710 + +régularisation . test variations 2: + formule: cotisation spéciale | du 01/02/2020 | au 29/02/2020 + exemples: + - valeur attendue: 0 From 97c984afef4952604169678e2ba6ebd4e30d0c47 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Thu, 12 Mar 2020 18:40:44 +0100 Subject: [PATCH 10/13] =?UTF-8?q?bar=C3=A8me=20et=20grille=20fonctionnent?= =?UTF-8?q?=20avec=20les=20variables=20temporelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/engine/mecanisms.js | 6 +- source/engine/mecanisms/barème.ts | 84 +++-- source/engine/mecanisms/grille.ts | 85 +++-- source/engine/mecanisms/trancheUtils.ts | 65 ++-- source/engine/mecanisms/variations.ts | 15 +- source/engine/temporal.ts | 424 +++++++++++++++++++++++ test/mécanismes/grille.yaml | 32 ++ test/mécanismes/variable-temporelle.yaml | 38 ++ test/temporal.test.js | 132 +++++++ 9 files changed, 800 insertions(+), 81 deletions(-) create mode 100644 source/engine/temporal.ts create mode 100644 test/temporal.test.js diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index 337e8cb31..1ed7e258b 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -3,7 +3,6 @@ import variations from 'Engine/mecanisms/variations' import { convertNodeToUnit } from 'Engine/nodeUnits' import { inferUnit, isPercentUnit } from 'Engine/units' import { - add, any, equals, evolve, @@ -342,7 +341,10 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => { export let mecanismSum = (recurse, k, v) => { let explanation = v.map(recurse) - let evaluate = evaluateArray(add, 0) + let evaluate = evaluateArray( + (x, y) => (x === false && y === false ? false : x + y), + false + ) return { evaluate, diff --git a/source/engine/mecanisms/barème.ts b/source/engine/mecanisms/barème.ts index 2ebd8aa4e..39cb80003 100644 --- a/source/engine/mecanisms/barème.ts +++ b/source/engine/mecanisms/barème.ts @@ -1,7 +1,10 @@ +import { evaluationError } from 'Engine/error' import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation' import { decompose } from 'Engine/mecanisms/utils' import variations from 'Engine/mecanisms/variations' import Barème from 'Engine/mecanismViews/Barème' +import { liftTemporalNode, mapTemporal, temporalAverage } from 'Engine/period' +import { liftTemporal2 } from 'Engine/temporal' import { convertUnit, parseUnit } from '../units' import { evaluatePlafondUntilActiveTranche, @@ -34,28 +37,18 @@ export default function parse(parse, k, v) { } } -const evaluate = ( - cache, - situationGate, - parsedRules, - node: ReturnType -) => { - const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) - const assiette = evaluate(node.explanation.assiette) - const multiplicateur = evaluate(node.explanation.multiplicateur) - const tranches = evaluatePlafondUntilActiveTranche( - evaluate, - { - parsedTranches: node.explanation.tranches, - assiette, - multiplicateur - }, - cache - ).map(tranche => { +function evaluateBarème(tranches, assiette, evaluate, cache) { + return tranches.map(tranche => { if (tranche.isAfterActive) { return { ...tranche, nodeValue: 0 } } const taux = evaluate(tranche.taux) + if (taux.temporalValue) { + evaluationError( + cache._meta.contextRule, + "Le taux d'une tranche ne peut pas être une valeur temporelle" + ) + } if ([taux.nodeValue, tranche.nodeValue].some(value => value === null)) { return { ...tranche, @@ -74,17 +67,60 @@ const evaluate = ( convertUnit(taux.unit, parseUnit(''), taux.nodeValue) } }) +} +const evaluate = ( + cache, + situationGate, + parsedRules, + node: ReturnType +) => { + const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) + const assiette = evaluate(node.explanation.assiette) + const multiplicateur = evaluate(node.explanation.multiplicateur) + const temporalTranchesPlafond = liftTemporal2( + (assiette, multiplicateur) => + evaluatePlafondUntilActiveTranche( + evaluate, + { + parsedTranches: node.explanation.tranches, + assiette, + multiplicateur + }, + cache + ), + liftTemporalNode(assiette), + liftTemporalNode(multiplicateur) + ) + const temporalTranches = liftTemporal2( + (tranches, assiette) => evaluateBarème(tranches, assiette, evaluate, cache), + temporalTranchesPlafond, + liftTemporalNode(assiette) + ) + + const temporalValue = mapTemporal( + tranches => + tranches.reduce( + (value, { nodeValue }) => + nodeValue == null ? null : value + nodeValue, + 0 + ), + temporalTranches + ) + return { ...node, - nodeValue: tranches.reduce( - (value, { nodeValue }) => (nodeValue == null ? null : value + nodeValue), - 0 - ), - missingVariables: mergeAllMissing(tranches), + nodeValue: temporalAverage(temporalValue), + ...(temporalValue.length > 1 + ? { + temporalValue + } + : { missingVariables: mergeAllMissing(temporalTranches[0].value) }), explanation: { assiette, multiplicateur, - tranches + ...(temporalTranches.length > 1 + ? { temporalTranches } + : { tranches: temporalTranches[0].value }) }, unit: assiette.unit } diff --git a/source/engine/mecanisms/grille.ts b/source/engine/mecanisms/grille.ts index 1fd819fdd..353e34c04 100644 --- a/source/engine/mecanisms/grille.ts +++ b/source/engine/mecanisms/grille.ts @@ -2,6 +2,8 @@ import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation' import { decompose } from 'Engine/mecanisms/utils' import variations from 'Engine/mecanisms/variations' import grille from 'Engine/mecanismViews/Grille' +import { liftTemporalNode, mapTemporal, temporalAverage } from 'Engine/period' +import { liftTemporal2 } from 'Engine/temporal' import { parseUnit } from 'Engine/units' import { lensPath, over } from 'ramda' import { @@ -35,25 +37,8 @@ export default function parse(parse, k, v) { unit: explanation.tranches[0].montant.unit } } - -const evaluate = ( - cache, - situationGate, - parsedRules, - node: ReturnType -) => { - const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) - const assiette = evaluate(node.explanation.assiette) - const multiplicateur = evaluate(node.explanation.multiplicateur) - const tranches = evaluatePlafondUntilActiveTranche( - evaluate, - { - parsedTranches: node.explanation.tranches, - assiette, - multiplicateur - }, - cache - ).map(tranche => { +const evaluateGrille = (tranches, evaluate) => + tranches.map(tranche => { if (tranche.isActive === false) { return tranche } @@ -67,19 +52,65 @@ const evaluate = ( } }) - const activeTranches = tranches.filter(({ isActive }) => isActive != false) - const missingVariables = mergeAllMissing(activeTranches) - const nodeValue = activeTranches.length ? activeTranches[0].nodeValue : false +const evaluate = ( + cache, + situationGate, + parsedRules, + node: ReturnType +) => { + const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) + const assiette = evaluate(node.explanation.assiette) + const multiplicateur = evaluate(node.explanation.multiplicateur) + const temporalTranchesPlafond = liftTemporal2( + (assiette, multiplicateur) => + evaluatePlafondUntilActiveTranche( + evaluate, + { + parsedTranches: node.explanation.tranches, + assiette, + multiplicateur + }, + cache + ), + liftTemporalNode(assiette), + liftTemporalNode(multiplicateur) + ) + const temporalTranches = mapTemporal( + tranches => evaluateGrille(tranches, evaluate), + temporalTranchesPlafond + ) + + const activeTranches = mapTemporal(tranches => { + const activeTranche = tranches.find(tranche => tranche.isActive) + if (activeTranche) { + return [activeTranche] + } + const lastTranche = tranches[tranches.length - 1] + if (lastTranche.isAfterActive === false) { + return [{ nodeValue: false }] + } + return tranches.filter(tranche => tranche.isActive === null) + }, temporalTranches) + const temporalValue = mapTemporal( + tranches => (tranches[0].isActive === null ? null : tranches[0].nodeValue), + activeTranches + ) return { ...node, + nodeValue: temporalAverage(temporalValue), + ...(temporalValue.length > 1 + ? { + temporalValue + } + : { missingVariables: mergeAllMissing(activeTranches[0].value) }), explanation: { - tranches, assiette, - multiplicateur + multiplicateur, + ...(temporalTranches.length > 1 + ? { temporalTranches } + : { tranches: temporalTranches[0].value }) }, - missingVariables, - nodeValue, - unit: activeTranches[0]?.unit ?? node.unit + unit: activeTranches[0].value[0]?.unit ?? node.unit } } diff --git a/source/engine/mecanisms/trancheUtils.ts b/source/engine/mecanisms/trancheUtils.ts index f0e493316..bed2fdfe0 100644 --- a/source/engine/mecanisms/trancheUtils.ts +++ b/source/engine/mecanisms/trancheUtils.ts @@ -7,7 +7,6 @@ export const parseTranches = (parse, tranches) => { return tranches .map((t, i) => { if (!t.plafond && i > tranches.length) { - console.log(t, i) throw new SyntaxError( `La tranche n°${i} du barème n'a pas de plafond précisé. Seule la dernière tranche peut ne pas être plafonnée` ) @@ -32,27 +31,25 @@ export function evaluatePlafondUntilActiveTranche( } const plafond = evaluate(parsedTranche.plafond) + if (plafond.temporalValue) { + evaluationError( + cache._meta.contextRule, + 'Les valeurs temporelles ne sont pas acceptées pour un plafond de tranche' + ) + } const plancher = tranches[i - 1] ? tranches[i - 1].plafond : { nodeValue: 0 } - const calculationValues = [plafond, assiette, multiplicateur, plancher] - if (calculationValues.some(node => node.nodeValue === null)) { - return [ - [ - ...tranches, - { - ...parsedTranche, - plafond, - nodeValue: null, - isActive: null, - isAfterActive: false, - missingVariables: mergeAllMissing(calculationValues) - } - ], - false - ] - } - let plafondValue = plafond.nodeValue * multiplicateur.nodeValue + const isAfterActive = + plancher.nodeValue === null || assiette.nodeValue === null + ? null + : plancher.nodeValue > assiette.nodeValue + + let plafondValue = + plafond.nodeValue === null || multiplicateur.nodeValue === null + ? null + : plafond.nodeValue * multiplicateur.nodeValue + try { plafondValue = [Infinity || 0].includes(plafondValue) ? plafondValue @@ -69,9 +66,33 @@ export function evaluatePlafondUntilActiveTranche( e ) } - let plancherValue = tranches[i - 1] ? tranches[i - 1].plafondValue : 0 - if (!!tranches[i - 1] && plafondValue <= plancherValue) { + + const calculationValues = [plafond, assiette, multiplicateur, plancher] + if (calculationValues.some(node => node.nodeValue === null)) { + return [ + [ + ...tranches, + { + ...parsedTranche, + plafond, + plafondValue, + plancherValue, + nodeValue: null, + isActive: null, + isAfterActive, + missingVariables: mergeAllMissing(calculationValues) + } + ], + false + ] + } + + if ( + !!tranches[i - 1] && + !!plancherValue && + plafondValue <= plancherValue + ) { evaluationError( cache._meta.contextRule, `Le plafond de la tranche n°${i + @@ -84,7 +105,7 @@ export function evaluatePlafondUntilActiveTranche( plafond, plancherValue, plafondValue, - isAfterActive: false, + isAfterActive, isActive: assiette.nodeValue >= plancherValue && assiette.nodeValue < plafondValue diff --git a/source/engine/mecanisms/variations.ts b/source/engine/mecanisms/variations.ts index 69659af98..3fa2c6bbd 100644 --- a/source/engine/mecanisms/variations.ts +++ b/source/engine/mecanisms/variations.ts @@ -4,13 +4,12 @@ import Variations from 'Engine/mecanismViews/Variations' import { convertNodeToUnit } from 'Engine/nodeUnits' import { liftTemporal2, - mapTemporal, pureTemporal, sometime, temporalAverage } from 'Engine/period' import { inferUnit } from 'Engine/units' -import { and, dissoc, not, or } from 'ramda' +import { dissoc, or } from 'ramda' import { mergeAllMissing } from './../evaluation' /* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */ @@ -81,12 +80,16 @@ function evaluate( } const evaluatedCondition = evaluate(condition) const currentCondition = liftTemporal2( - and, - mapTemporal(not, previousConditions), + (previousCond, currentCond) => + previousCond === null ? previousCond : !previousCond && currentCond, + previousConditions, evaluatedCondition.temporalValue ?? pureTemporal(evaluatedCondition.nodeValue) ) - const currentConditionAlwaysFalse = !sometime(Boolean, currentCondition) + const currentConditionAlwaysFalse = !sometime( + x => x !== false, + currentCondition + ) if (currentConditionAlwaysFalse) { return [ evaluation, @@ -112,7 +115,6 @@ function evaluate( evaluatedConsequence.temporalValue ?? pureTemporal(evaluatedConsequence.nodeValue) ) - return [ liftTemporal2(or, evaluation, currentValue), [ @@ -139,6 +141,7 @@ function evaluate( [] ) ) + // console.log(missingVariables, nodeValue, temporalValue) return { ...node, nodeValue, diff --git a/source/engine/temporal.ts b/source/engine/temporal.ts new file mode 100644 index 000000000..cee75af2e --- /dev/null +++ b/source/engine/temporal.ts @@ -0,0 +1,424 @@ +import { + convertToDate, + getDifferenceInDays, + getDifferenceInMonths, + getDifferenceInYears, + getRelativeDate, + getYear +} from 'Engine/date' +import { Unit } from './units' + +export type Period = { + start: T | null + end: T | null +} + +export function parsePeriod(word: string, date: Date): Period { + const startWords = [ + 'depuis', + 'depuis le', + 'depuis la', + 'à partir de', + 'à partir du', + 'du' + ] + const endWords = [ + "jusqu'à", + "jusqu'au", + "jusqu'à la", + 'avant', + 'avant le', + 'avant la', + 'au' + ] + const intervalWords = ['le', 'en'] + if (!startWords.concat(endWords, intervalWords).includes(word)) { + throw new SyntaxError( + `Le mot clé '${word}' n'est pas valide. Les mots clés possible sont les suivants :\n\t ${startWords.join( + ', ' + )}` + ) + } + if (word === 'le') { + return { + start: date, + end: date + } + } + if (word === 'en') { + return { start: null, end: null } + } + if (startWords.includes(word)) { + return { + start: date, + end: null + } + } + if (endWords.includes(word)) { + return { + start: null, + end: date + } + } + throw new Error('Non implémenté') +} + +// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable) +// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)] +export type Evaluation = T | false | null + +export type EvaluatedNode = { + nodeValue: Evaluation + temporalValue?: Temporal> +} + +export type TemporalNode = Temporal<{ nodeValue: Evaluation }> +export type Temporal = Array & { value: T }> + +export function narrowTemporalValue( + period: Period, + temporalValue: Temporal> +): Temporal> { + return liftTemporal2( + (value, filter) => filter && value, + temporalValue, + createTemporalEvaluation(true, period) + ) +} + +// Returns a temporal value that's true for the given period and false otherwise. +export function createTemporalEvaluation( + value: Evaluation, + period: Period = { start: null, end: null } +): Temporal> { + let temporalValue = [{ ...period, value }] + if (period.start != null) { + temporalValue.unshift({ + start: null, + end: getRelativeDate(period.start, -1), + value: false + }) + } + if (period.end != null) { + temporalValue.push({ + start: getRelativeDate(period.end, 1), + end: null, + value: false + }) + } + return temporalValue +} + +export function pureTemporal(value: T): Temporal { + return [{ start: null, end: null, value }] +} + +export function mapTemporal( + fn: (value: T1) => T2, + temporalValue: Temporal +): Temporal { + return temporalValue.map(({ start, end, value }) => ({ + start, + end, + value: fn(value) + })) +} +export function sometime( + fn: (value: T1) => boolean, + temporalValue: Temporal +): boolean { + return temporalValue.some(({ start, end, value }) => fn(value)) +} + +export function liftTemporal2( + fn: (value1: T1, value2: T2) => T3, + temporalValue1: Temporal, + temporalValue2: Temporal +): Temporal { + return mapTemporal( + ([a, b]) => fn(a, b), + zipTemporals(temporalValue1, temporalValue2) + ) +} + +export function concatTemporals( + temporalValues: Array> +): Temporal> { + return temporalValues.reduce( + (values, value) => liftTemporal2((a, b) => [...a, b], values, value), + pureTemporal([]) as Temporal> + ) +} + +export function liftTemporalNode(node: EvaluatedNode): TemporalNode { + const { temporalValue, ...baseNode } = node + if (!temporalValue) { + return pureTemporal(baseNode) + } + return mapTemporal( + nodeValue => ({ + ...baseNode, + nodeValue + }), + temporalValue + ) +} + +export function zipTemporals( + temporalValue1: Temporal, + temporalValue2: Temporal, + acc: Temporal<[T1, T2]> = [] +): Temporal<[T1, T2]> { + if (!temporalValue1.length && !temporalValue2.length) { + return acc + } + const [value1, ...rest1] = temporalValue1 + const [value2, ...rest2] = temporalValue2 + console.assert(value1.start === value2.start) + const endDateComparison = compareEndDate(value1.end, value2.end) + + // End dates are equals + if (endDateComparison === 0) { + return zipTemporals(rest1, rest2, [ + ...acc, + { ...value1, value: [value1.value, value2.value] } + ]) + } + // Value1 lasts longuer than value1 + if (endDateComparison > 0) { + console.assert(value2.end !== null) + return zipTemporals( + [ + { ...value1, start: getRelativeDate(value2.end as string, 1) }, + ...rest1 + ], + rest2, + [ + ...acc, + { + ...value2, + value: [value1.value, value2.value] + } + ] + ) + } + + // Value2 lasts longuer than value1 + if (endDateComparison < 0) { + console.assert(value1.end !== null) + return zipTemporals( + rest1, + [ + { ...value2, start: getRelativeDate(value1.end as string, 1) }, + ...rest2 + ], + [ + ...acc, + { + ...value1, + value: [value1.value, value2.value] + } + ] + ) + } + throw new EvalError('All case should have been covered') +} + +function beginningOfNextYear(date: string): string { + return `01/01/${getYear(date) + 1}` +} + +function endsOfPreviousYear(date: string): string { + return `31/12/${getYear(date) - 1}` +} + +function splitStartsAt( + fn: (date: string) => string, + temporal: Temporal +): Temporal { + return temporal.reduce((acc, period) => { + const { start, end } = period + const newStart = start === null ? start : fn(start) + if (compareEndDate(newStart, end) !== -1) { + return [...acc, period] + } + console.assert(newStart !== null) + return [ + ...acc, + { ...period, end: getRelativeDate(newStart as string, -1) }, + { ...period, start: newStart } + ] + }, [] as Temporal) +} + +function splitEndsAt( + fn: (date: string) => string, + temporal: Temporal +): Temporal { + return temporal.reduce((acc, period) => { + const { start, end } = period + const newEnd = end === null ? end : fn(end) + if (compareStartDate(start, newEnd) !== -1) { + return [...acc, period] + } + console.assert(newEnd !== null) + return [ + ...acc, + { ...period, end: newEnd }, + { ...period, start: getRelativeDate(newEnd as string, 1) } + ] + }, [] as Temporal) +} + +export function groupByYear(temporalValue: Temporal): Array> { + return ( + // First step: split period by year if needed + splitEndsAt( + endsOfPreviousYear, + splitStartsAt(beginningOfNextYear, temporalValue) + ) + // Second step: group period by year + .reduce((acc, period) => { + const [currentTemporal, ...otherTemporal] = acc + if (currentTemporal === undefined) { + return [[period]] + } + const firstPeriod = currentTemporal[0] + console.assert( + firstPeriod !== undefined && + firstPeriod.end !== null && + period.start !== null, + 'invariant non verifié' + ) + if ( + (firstPeriod.end as string).slice(-4) !== + (period.start as string).slice(-4) + ) { + return [[period], ...acc] + } + return [[...currentTemporal, period], ...otherTemporal] + }, [] as Array>) + .reverse() + ) +} + +function simplify(temporalValue: Temporal): Temporal { + return temporalValue +} + +function compareStartDate( + dateA: string | null, + dateB: string | null +): -1 | 0 | 1 { + if (dateA == dateB) { + return 0 + } + if (dateA == null) { + return -1 + } + if (dateB == null) { + return 1 + } + return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1 +} + +function compareEndDate( + dateA: string | null, + dateB: string | null +): -1 | 0 | 1 { + if (dateA == dateB) { + return 0 + } + if (dateA == null) { + return 1 + } + if (dateB == null) { + return -1 + } + return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1 +} + +export function temporalAverage( + temporalValue: Temporal>, + unit?: Unit +): Evaluation { + temporalValue = temporalValue.filter(({ value }) => value !== false) + const first = temporalValue[0] + const last = temporalValue[temporalValue.length - 1] + if (!temporalValue.length) { + return false + } + if (temporalValue.length === 1) { + return temporalValue[0].value + } + + // La variable est définie sur un interval infini + if (first.start == null || last.end == null) { + if (first.start != null) { + return last.value + } + if (last.end != null) { + return first.value + } + return (first.value + last.value) / 2 + } + + if (temporalValue.some(({ value }) => value == null)) { + return null + } + let totalWeight = 0 + const weights = temporalValue.map(({ start, end, value }) => { + let weight = 0 + if (unit?.denominators.includes('mois')) { + weight = getDifferenceInMonths(start, end) + } else if (unit?.denominators.includes('année')) { + weight = getDifferenceInYears(start, end) + } else { + weight = getDifferenceInDays(start, end) + } + totalWeight += weight + return (value as number) * weight + }) + return weights.reduce( + (average, weightedValue) => average + weightedValue / totalWeight, + 0 + ) +} + +export function temporalCumul( + temporalValue: Temporal>, + unit: Unit +): Evaluation { + temporalValue = temporalValue.filter(({ value }) => value !== false) + const first = temporalValue[0] + const last = temporalValue[temporalValue.length - 1] + if (!temporalValue.length) { + return false + } + + // La variable est définie sur un interval infini + if (first.start == null || last.end == null) { + if (first.start != null) { + return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity + } + if (last.end != null) { + return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity + } + return null + } + if (temporalValue.some(({ value }) => value == null)) { + return null + } + + return temporalValue.reduce((acc, { start, end, value }) => { + let weight = 1 + if (unit?.denominators.includes('mois')) { + weight = getDifferenceInMonths(start, end) + } else if (unit?.denominators.includes('année')) { + weight = getDifferenceInYears(start, end) + } else if (unit?.denominators.includes('jour')) { + weight = getDifferenceInDays(start, end) + } + return value * weight + acc + }, 0) +} diff --git a/test/mécanismes/grille.yaml b/test/mécanismes/grille.yaml index 30402c554..c3af15e9b 100644 --- a/test/mécanismes/grille.yaml +++ b/test/mécanismes/grille.yaml @@ -31,3 +31,35 @@ Grille: situation: assiette: 999.3 valeur attendue: 50 + +plafond: + unité: € +Grille avec valeur manquante: + formule: + grille: + assiette: assiette + unité: € + tranches: + - montant: 100 + plafond: plafond + - montant: 200 + plafond: 2000 € + - montant: 300 + plafond: 4000 € + + unité attendue: € + exemples: + - nom: 'variable manquante' + situation: + assiette: 1000 + variables manquantes: + - plafond + valeur attendue: null + - nom: 'assiette non concernée par variable manquante' + situation: + assiette: 3000 + valeur attendue: 300 + - nom: 'assiette au delà du plagond' + situation: + assiette: 5000 + valeur attendue: false diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml index 80fce38c4..523f81af8 100644 --- a/test/mécanismes/variable-temporelle.yaml +++ b/test/mécanismes/variable-temporelle.yaml @@ -211,3 +211,41 @@ variable temporelle numérique . multiplication avec valeur sur l'année: # formule: salaire brut [avril 2019] # exemples: # - valeur attendue: 400 # (2000 * 6)/30 +cotisation spéciale: + formule: + barème: + assiette: contrat salarié . salaire + multiplicateur: plafond sécurité sociale + tranches: + - taux: 0% + plafond: 10% + - taux: 10% + plafond: 20% + - taux: 30% + plafond: 50% + - taux: 40% + plafond: 100% + - taux: 50% + +variable temporelle numérique . barème: + formule: cotisation spéciale | du 01/01/2019 | au 31/12/2019 + exemples: + - valeur attendue: 567.438 + +grille: + formule: + barème: + assiette: contrat salarié . salaire + tranches: + - montant: 5 heures + plafond: 1000€ + - montant: 10 heures + plafond: 2000 € + - montant: 30 heures + plafond: 4000 € + - montant: 40 heures + +variable temporelle numérique . grille: + formule: cotisation spéciale | du 01/01/2019 | au 31/12/2019 + exemples: + - valeur attendue: 567.438 diff --git a/test/temporal.test.js b/test/temporal.test.js new file mode 100644 index 000000000..4316b3869 --- /dev/null +++ b/test/temporal.test.js @@ -0,0 +1,132 @@ +import { expect } from 'chai' +import { + concatTemporals, + createTemporalEvaluation, + groupByYear, + zipTemporals +} from '../source/engine/temporal' + +const neverEnding = value => [{ start: null, end: null, value: value }] +describe('Periods : zip', () => { + it('should zip two empty temporalValue', () => { + const result = zipTemporals([], []) + expect(result).to.deep.equal([]) + }) + + it('should zip constant temporalValue', () => { + const result = zipTemporals(neverEnding(1), neverEnding(2)) + expect(result).to.deep.equal(neverEnding([1, 2])) + }) + + it('should zip changing temporalValue', () => { + const value1 = createTemporalEvaluation(true, { + start: null, + end: '01/08/2020' + }) + const value2 = neverEnding(1) + expect(zipTemporals(value1, value2)).to.deep.equal([ + { start: null, end: '01/08/2020', value: [true, 1] }, + { start: '02/08/2020', end: null, value: [false, 1] } + ]) + expect(zipTemporals(value2, value1)).to.deep.equal([ + { start: null, end: '01/08/2020', value: [1, true] }, + { start: '02/08/2020', end: null, value: [1, false] } + ]) + }) + + it('should zip two overlapping temporalValue', () => { + const value1 = createTemporalEvaluation(1, { + start: '01/07/2019', + end: '30/06/2020' + }) + const value2 = createTemporalEvaluation(2, { + start: '01/01/2019', + end: '31/12/2019' + }) + + expect(zipTemporals(value1, value2)).to.deep.equal([ + { start: null, end: '31/12/2018', value: [false, false] }, + { start: '01/01/2019', end: '30/06/2019', value: [false, 2] }, + { start: '01/07/2019', end: '31/12/2019', value: [1, 2] }, + { start: '01/01/2020', end: '30/06/2020', value: [1, false] }, + { start: '01/07/2020', end: null, value: [false, false] } + ]) + }) +}) + +describe('Periods : concat', () => { + it('should merge concat overlapping temporalValue', () => { + const value1 = createTemporalEvaluation(10) + const value2 = [ + { start: null, end: '14/04/2019', value: 100 }, + { start: '15/04/2019', end: '08/08/2019', value: 2000 }, + { start: '09/08/2019', end: null, value: 200 } + ] + + expect(concatTemporals([value1, value2])).to.deep.equal([ + { start: null, end: '14/04/2019', value: [10, 100] }, + { start: '15/04/2019', end: '08/08/2019', value: [10, 2000] }, + { start: '09/08/2019', end: null, value: [10, 200] } + ]) + }) +}) + +describe('Periods : groupByYear', () => { + const invariants = temporalYear => { + const startDate = temporalYear[0].start + const endDate = temporalYear.slice(-1)[0].end + expect( + startDate === null || startDate.startsWith('01/01'), + 'starts at the beginning of a year' + ) + expect( + endDate === null || endDate.startsWith('31/12'), + 'stops at the end of a year' + ) + } + it('should handle constant value', () => { + const value = createTemporalEvaluation(10) + expect(groupByYear(value)).to.deep.equal([value]) + }) + it('should handle changing value', () => { + const value = createTemporalEvaluation(10, { + start: '06/06/2020', + end: '20/12/2020' + }) + const result = groupByYear(value) + expect(result).to.have.length(3) + result.forEach(invariants) + }) + it('should handle changing value over several years', () => { + const value = createTemporalEvaluation(10, { + start: '06/06/2020', + end: '20/12/2022' + }) + const result = groupByYear(value) + expect(result).to.have.length(5) + result.forEach(invariants) + }) + it('should handle complex case', () => { + const result = groupByYear( + concatTemporals([ + createTemporalEvaluation(1, { + start: '06/06/2020', + end: '20/12/2022' + }), + createTemporalEvaluation(2, { + start: '01/01/1991', + end: '20/12/1992' + }), + createTemporalEvaluation(3, { + start: '31/01/1990', + end: '20/12/2021' + }), + createTemporalEvaluation(4, { + start: '31/12/2020', + end: '01/01/2021' + }) + ]) + ) + result.forEach(invariants) + }) +}) From 467482031c92921ea422196f6a586b4dad8286d0 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Mon, 16 Mar 2020 11:31:32 +0100 Subject: [PATCH 11/13] =?UTF-8?q?:white=5Fcheck=5Fmark:=20corrige=20les=20?= =?UTF-8?q?erreurs=20sur=20les=20bar=C3=A8me=20et=20les=20missing=20variab?= =?UTF-8?q?les?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- publicode/rules/impôt.yaml | 2 +- publicode/rules/salarié.yaml | 3 +-- source/components/PaySlipSections.js | 15 +++++++++++---- source/engine/evaluation.tsx | 9 +++++---- source/engine/mecanismViews/Variations.js | 6 +++--- source/engine/mecanisms/barème.ts | 11 +++++++++-- source/engine/mecanisms/operation.js | 2 +- source/engine/mecanisms/trancheUtils.ts | 8 ++++---- source/engine/mecanisms/variations.ts | 5 ++--- test/library.test.js | 8 ++++---- test/mécanismes/régularisation.yaml | 2 +- test/rules/sasu.yaml | 4 +++- 12 files changed, 45 insertions(+), 30 deletions(-) diff --git a/publicode/rules/impôt.yaml b/publicode/rules/impôt.yaml index c24c46f21..cd125770e 100644 --- a/publicode/rules/impôt.yaml +++ b/publicode/rules/impôt.yaml @@ -12,7 +12,6 @@ impôt: produit: assiette: revenu imposable taux: taux du prélèvement à la source - - sinon: 0 - CEHR - dirigeant . auto-entrepreneur . impôt . versement libératoire . montant @@ -358,6 +357,7 @@ impôt . taux personnalisé: revenus net de cotisations: résumé: Avant impôt + unité: €/an question: Quel revenu avant impôt voulez-vous toucher ? description: | Il s'agit du revenu net de cotisations et de charges, avant le paiement de l'impôt sur le revenu. diff --git a/publicode/rules/salarié.yaml b/publicode/rules/salarié.yaml index c8685d5f9..a0f710a24 100644 --- a/publicode/rules/salarié.yaml +++ b/publicode/rules/salarié.yaml @@ -2898,13 +2898,12 @@ contrat salarié . taxe sur les salaires: entreprise . effectif: 1 valeur attendue: 0 - nom: association non lucrative - unités par défaut: [€/mois] situation: entreprise . association non lucrative: oui rémunération . brut de base: 2300 entreprise . effectif: 10 complémentaire santé . forfait: 0 - valeur attendue: 48.10 + valeur attendue: 577 références: fiche: https://www.service-public.fr/professionnels-entreprises/vosdroits/F22576 diff --git a/source/components/PaySlipSections.js b/source/components/PaySlipSections.js index 0c72d71fd..818ca3be1 100644 --- a/source/components/PaySlipSections.js +++ b/source/components/PaySlipSections.js @@ -47,10 +47,17 @@ export let SalaireBrutSection = ({ getRule }) => { export let Line = ({ rule, ...props }) => { const defaultUnit = useSelector(defaultUnitSelector) - ;<> - - - + return ( + <> + + + + ) } export let SalaireNetSection = ({ getRule }) => { diff --git a/source/engine/evaluation.tsx b/source/engine/evaluation.tsx index 216a264fc..244bdda7e 100644 --- a/source/engine/evaluation.tsx +++ b/source/engine/evaluation.tsx @@ -8,6 +8,7 @@ import { mergeWith, reduce } from 'ramda' +import React from 'react' import { typeWarning } from './error' import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits' import { @@ -79,9 +80,7 @@ export const evaluateArray = (reducer, start) => ( cache._meta.contextRule, node.name ) - if (!evaluatedNodes.every(Boolean)) { - console.log(node.explanation) - } + const temporalValues = concatTemporals( evaluatedNodes.map( ({ temporalValue, nodeValue }) => temporalValue ?? pureTemporal(nodeValue) @@ -96,6 +95,7 @@ export const evaluateArray = (reducer, start) => ( const baseEvaluation = { ...node, + missingVariables: mergeAllMissing(evaluatedNodes), explanation: evaluatedNodes, unit: evaluatedNodes[0].unit } @@ -191,7 +191,8 @@ export let evaluateObject = (objectShape, effect) => ( ...node, nodeValue, unit: sameUnitTemporalExplanation[0].value.unit, - explanation: evaluations + explanation: evaluations, + missingVariables: mergeAllMissing(Object.values(evaluations)) } if (sameUnitTemporalExplanation.length === 1) { return { diff --git a/source/engine/mecanismViews/Variations.js b/source/engine/mecanismViews/Variations.js index cf895a072..4a281eee9 100644 --- a/source/engine/mecanismViews/Variations.js +++ b/source/engine/mecanismViews/Variations.js @@ -11,7 +11,6 @@ import './Variations.css' let Comp = function Variations({ nodeValue, explanation, unit }) { let [expandedVariation, toggleVariation] = useState(null) const { i18n } = useTranslation() - return ( {showValues => ( @@ -32,6 +31,7 @@ let Comp = function Variations({ nodeValue, explanation, unit }) {

    {explanation.map(({ condition, consequence, satisfied }, i) => ( + // console.log(condition, satisfied) ||
  1. - {condition && ( + {!condition.isDefault && (
    - {condition ? ( + {!condition.isDefault ? ( Alors ) : ( Sinon diff --git a/source/engine/mecanisms/barème.ts b/source/engine/mecanisms/barème.ts index 39cb80003..ba7f26f73 100644 --- a/source/engine/mecanisms/barème.ts +++ b/source/engine/mecanisms/barème.ts @@ -49,7 +49,15 @@ function evaluateBarème(tranches, assiette, evaluate, cache) { "Le taux d'une tranche ne peut pas être une valeur temporelle" ) } - if ([taux.nodeValue, tranche.nodeValue].some(value => value === null)) { + + if ( + [ + assiette.nodeValue, + taux.nodeValue, + tranche.plafondValue, + tranche.plancherValue + ].some(value => value === null) + ) { return { ...tranche, taux, @@ -96,7 +104,6 @@ const evaluate = ( temporalTranchesPlafond, liftTemporalNode(assiette) ) - const temporalValue = mapTemporal( tranches => tranches.reduce( diff --git a/source/engine/mecanisms/operation.js b/source/engine/mecanisms/operation.js index 3bfed3083..1cbc2d200 100644 --- a/source/engine/mecanisms/operation.js +++ b/source/engine/mecanisms/operation.js @@ -57,7 +57,7 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => { if (['∕', '-'].includes(node.operator) && a === false) { return false } - if (['×', '+'].includes(node.operator) && a === false) { + if (['+'].includes(node.operator) && a === false) { return b } if (['∕', '-', '×', '+'].includes(node.operator) && b === false) { diff --git a/source/engine/mecanisms/trancheUtils.ts b/source/engine/mecanisms/trancheUtils.ts index bed2fdfe0..6ebb1a85a 100644 --- a/source/engine/mecanisms/trancheUtils.ts +++ b/source/engine/mecanisms/trancheUtils.ts @@ -40,10 +40,6 @@ export function evaluatePlafondUntilActiveTranche( const plancher = tranches[i - 1] ? tranches[i - 1].plafond : { nodeValue: 0 } - const isAfterActive = - plancher.nodeValue === null || assiette.nodeValue === null - ? null - : plancher.nodeValue > assiette.nodeValue let plafondValue = plafond.nodeValue === null || multiplicateur.nodeValue === null @@ -67,6 +63,10 @@ export function evaluatePlafondUntilActiveTranche( ) } let plancherValue = tranches[i - 1] ? tranches[i - 1].plafondValue : 0 + const isAfterActive = + plancherValue === null || assiette.nodeValue === null + ? null + : plancherValue > assiette.nodeValue const calculationValues = [plafond, assiette, multiplicateur, plancher] if (calculationValues.some(node => node.nodeValue === null)) { diff --git a/source/engine/mecanisms/variations.ts b/source/engine/mecanisms/variations.ts index 3fa2c6bbd..84ec8d851 100644 --- a/source/engine/mecanisms/variations.ts +++ b/source/engine/mecanisms/variations.ts @@ -115,12 +115,14 @@ function evaluate( evaluatedConsequence.temporalValue ?? pureTemporal(evaluatedConsequence.nodeValue) ) + console.log(evaluatedCondition) return [ liftTemporal2(or, evaluation, currentValue), [ ...explanations, { condition: evaluatedCondition, + satisfied: !!evaluatedCondition.nodeValue, consequence: evaluatedConsequence } ], @@ -141,7 +143,6 @@ function evaluate( [] ) ) - // console.log(missingVariables, nodeValue, temporalValue) return { ...node, nodeValue, @@ -149,7 +150,5 @@ function evaluate( explanation, missingVariables, ...(temporalValue.length > 1 && { temporalValue }) - // TODO - // missingVariables } } diff --git a/test/library.test.js b/test/library.test.js index 1b480c1df..0a8ff82e4 100644 --- a/test/library.test.js +++ b/test/library.test.js @@ -88,13 +88,13 @@ impôt sur le revenu: assiette: revenu abattu tranches: - taux: 0% - plafond: 9807 + plafond: 9807 € - taux: 14% - plafond: 27086 + plafond: 27086 € - taux: 30% - plafond: 72617 + plafond: 72617 € - taux: 41% - plafond: 153783 + plafond: 153783 € - taux: 45% impôt sur le revenu à payer: diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml index 25396bad4..73178b473 100644 --- a/test/mécanismes/régularisation.yaml +++ b/test/mécanismes/régularisation.yaml @@ -93,4 +93,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 + - valeur attendue: 660 diff --git a/test/rules/sasu.yaml b/test/rules/sasu.yaml index 1afe5ab0f..c4c345a88 100644 --- a/test/rules/sasu.yaml +++ b/test/rules/sasu.yaml @@ -4,9 +4,11 @@ chiffre affaires: 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 From 6faa912aa77ffdd235a6c91ddbae2058a82a27b5 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Mon, 16 Mar 2020 18:09:41 +0100 Subject: [PATCH 12/13] =?UTF-8?q?r=C3=A9pare=20le=20changement=20de=20p?= =?UTF-8?q?=C3=A9riodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/components/conversation/DateInput.tsx | 1 - .../conversation/InputSuggestions.tsx | 4 +-- source/engine/evaluation.tsx | 4 +-- source/engine/mecanismViews/Variations.js | 1 - source/engine/mecanisms/barème.ts | 4 +-- source/engine/mecanisms/régularisation.ts | 1 + source/engine/mecanisms/variations.ts | 1 - source/engine/nodeUnits.ts | 3 +- source/engine/parseRule.tsx | 3 +- source/reducers/rootReducer.ts | 36 +++++++++++++++++++ source/selectors/analyseSelectors.ts | 26 +++++++------- .../Récapitulatif.tsx | 1 - test/bug-cotisations.test.js | 1 - test/conversation.test.js | 26 +++++++++++--- test/ficheDePaieSelector.test.js | 1 + test/generateQuestions.test.js | 2 -- test/mécanismes/régularisation.yaml | 2 ++ 17 files changed, 82 insertions(+), 35 deletions(-) diff --git a/source/components/conversation/DateInput.tsx b/source/components/conversation/DateInput.tsx index c4322772f..aa1f337d9 100644 --- a/source/components/conversation/DateInput.tsx +++ b/source/components/conversation/DateInput.tsx @@ -13,7 +13,6 @@ export default function DateInput({ suggestions, onChange, onSubmit, value }) { const handleDateChange = useCallback( evt => { - console.log('target', evt.target) if (!evt.target.value) { return onChange(null) } diff --git a/source/components/conversation/InputSuggestions.tsx b/source/components/conversation/InputSuggestions.tsx index 02f3e3af7..c19a173a8 100644 --- a/source/components/conversation/InputSuggestions.tsx +++ b/source/components/conversation/InputSuggestions.tsx @@ -2,7 +2,7 @@ import { toPairs } from 'ramda' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { defaultUnitsSelector } from 'Selectors/analyseSelectors' +import { defaultUnitSelector } from 'Selectors/analyseSelectors' import { convertUnit, parseUnit, Unit } from '../../engine/units' type InputSuggestionsProps = { @@ -20,7 +20,7 @@ export default function InputSuggestions({ }: InputSuggestionsProps) { const [suggestion, setSuggestion] = useState() const { t } = useTranslation() - const defaultUnit = parseUnit(useSelector(defaultUnitsSelector)[0] ?? '') + const defaultUnit = parseUnit(useSelector(defaultUnitSelector) ?? '') if (!suggestions) return null return ( diff --git a/source/engine/evaluation.tsx b/source/engine/evaluation.tsx index 244bdda7e..ecfa9d32c 100644 --- a/source/engine/evaluation.tsx +++ b/source/engine/evaluation.tsx @@ -184,9 +184,7 @@ export let evaluateObject = (objectShape, effect) => ( sameUnitTemporalExplanation ) const nodeValue = temporalAverage(temporalValue) - if (nodeValue === 495) { - console.log(temporalValue) - } + const baseEvaluation = { ...node, nodeValue, diff --git a/source/engine/mecanismViews/Variations.js b/source/engine/mecanismViews/Variations.js index 4a281eee9..fac9ab951 100644 --- a/source/engine/mecanismViews/Variations.js +++ b/source/engine/mecanismViews/Variations.js @@ -31,7 +31,6 @@ let Comp = function Variations({ nodeValue, explanation, unit }) {

      {explanation.map(({ condition, consequence, satisfied }, i) => ( - // console.log(condition, satisfied) ||
    1. > diff --git a/source/engine/mecanisms/variations.ts b/source/engine/mecanisms/variations.ts index 84ec8d851..2b06b8cb9 100644 --- a/source/engine/mecanisms/variations.ts +++ b/source/engine/mecanisms/variations.ts @@ -115,7 +115,6 @@ function evaluate( evaluatedConsequence.temporalValue ?? pureTemporal(evaluatedConsequence.nodeValue) ) - console.log(evaluatedCondition) return [ liftTemporal2(or, evaluation, currentValue), [ diff --git a/source/engine/nodeUnits.ts b/source/engine/nodeUnits.ts index d8e24ad09..c56fd7728 100644 --- a/source/engine/nodeUnits.ts +++ b/source/engine/nodeUnits.ts @@ -7,10 +7,11 @@ import { } from './units' export function simplifyNodeUnit(node) { - if (!node.unit || !node.nodeValue) { + if (!node.unit || node.nodeValue === false || node.nodeValue == null) { return node } const [unit, nodeValue] = simplifyUnitWithValue(node.unit, node.nodeValue) + return { ...node, unit, diff --git a/source/engine/parseRule.tsx b/source/engine/parseRule.tsx index ef9ab3ca6..bfdb7ab0d 100644 --- a/source/engine/parseRule.tsx +++ b/source/engine/parseRule.tsx @@ -84,6 +84,7 @@ export default (rules, rule, parsedRules) => { node.explanation ), { nodeValue, unit, missingVariables, temporalValue } = explanation + return { ...node, nodeValue, @@ -136,8 +137,8 @@ export default (rules, rule, parsedRules) => { ...parsedRoot, evaluate, parsed: true, - isDisabledBy: [], defaultUnit: parsedRoot.defaultUnit || parsedRoot.formule?.unit, + isDisabledBy: [], replacedBy: [] } parsedRules[rule.dottedName]['rendu non applicable'] = { diff --git a/source/reducers/rootReducer.ts b/source/reducers/rootReducer.ts index 07fa0aaa1..d781ec1f1 100644 --- a/source/reducers/rootReducer.ts +++ b/source/reducers/rootReducer.ts @@ -8,6 +8,7 @@ import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' import { SavedSimulation } from 'Selectors/storageSelectors' import { DottedName } from 'Types/rule' import i18n, { AvailableLangs } from '../i18n' +import { areUnitConvertible, convertUnit, parseUnit } from './../engine/units' import inFranceAppReducer, { Company } from './inFranceAppReducer' import storageRootReducer from './storageReducer' @@ -110,6 +111,37 @@ function updateSituation( return { ...removePreviousTarget(situation), [fieldName]: value } } +function updateDefaultUnit(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' @@ -220,6 +252,10 @@ function simulation( case 'UPDATE_DEFAULT_UNIT': return { ...state, + situation: updateDefaultUnit(state.situation, { + toUnit: action.defaultUnit, + analysis + }), defaultUnit: action.defaultUnit } } diff --git a/source/selectors/analyseSelectors.ts b/source/selectors/analyseSelectors.ts index ffacc073e..da42e26b0 100644 --- a/source/selectors/analyseSelectors.ts +++ b/source/selectors/analyseSelectors.ts @@ -111,7 +111,7 @@ let validatedStepsSelector = createSelector( (foldedSteps, targetNames) => [...(foldedSteps || []), ...targetNames] ) export const defaultUnitSelector = (state: RootState) => - state.simulation?.defaultUnit + state.simulation?.defaultUnit ?? '€/mois' let branchesSelector = (state: RootState) => configSelector(state).branches let configSituationSelector = (state: RootState) => configSelector(state).situation || {} @@ -172,9 +172,9 @@ export let ruleAnalysisSelector = createSelector( (_, props: { dottedName: DottedName }) => props.dottedName, situationsWithDefaultsSelector, state => state.situationBranch || 0, - defaultUnitsSelector + defaultUnitSelector ], - (rules, dottedName, situations, situationBranch, defaultUnits) => { + (rules, dottedName, situations, situationBranch, defaultUnit) => { return analyseRule( rules, dottedName, @@ -184,7 +184,7 @@ export let ruleAnalysisSelector = createSelector( : situations return currentSituation[dottedName] }, - defaultUnits + [defaultUnit] ) } ) @@ -217,7 +217,7 @@ export let exampleAnalysisSelector = createSelector( rules, dottedName, (dottedName: DottedName) => situation[dottedName], - example?.defaultUnits + example?.defaultUnit ) ) @@ -227,18 +227,16 @@ let makeAnalysisSelector = (situationSelector: SituationSelectorType) => parsedRulesSelector, targetNamesSelector, situationSelector, - defaultUnitsSelector + defaultUnitSelector ], - (parsedRules, targetNames, situations, defaultUnits) => { + (parsedRules, targetNames, situations, defaultUnit) => { return mapOrApply( situation => - analyseMany( - parsedRules, - targetNames, - defaultUnits - )((dottedName: DottedName) => { - return situation[dottedName] - }), + analyseMany(parsedRules, targetNames, [defaultUnit])( + (dottedName: DottedName) => { + return situation[dottedName] + } + ), situations ) } 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 index 0790f69d2..e0e69ea19 100644 --- 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 @@ -12,7 +12,6 @@ export function AideDéclarationIndépendantsRécapitulatif() { const siren = useSelector( (state: RootState) => state.inFranceApp.existingCompany?.siren ) - console.log(useSelector((state: RootState) => state.rules)) const componentRef = useRef(null) return ( diff --git a/test/bug-cotisations.test.js b/test/bug-cotisations.test.js index c7ac3a7c9..72c5963a3 100644 --- a/test/bug-cotisations.test.js +++ b/test/bug-cotisations.test.js @@ -55,7 +55,6 @@ describe('bug-analyse-many', function() { const one = analyse(rules, 'cotisations')(stateSelector).targets[0] - //console.log(many[0].nodeValue, many[1].nodeValue, one.nodeValue) expect(many[1].nodeValue).to.be.closeTo(one.nodeValue, 0.1) }) it('should compute the same contributions if asked with analyseMany or analyse', function() { diff --git a/test/conversation.test.js b/test/conversation.test.js index dc64fddcf..2e041e956 100644 --- a/test/conversation.test.js +++ b/test/conversation.test.js @@ -8,7 +8,7 @@ import { nextStepsSelector } from '../source/selectors/analyseSelectors' let baseState = { - simulation: { situation: {}, foldedSteps: [] } + simulation: { defaultUnit: '€/an', situation: {}, foldedSteps: [] } } describe('conversation', function() { @@ -24,7 +24,11 @@ describe('conversation', function() { rules = rawRules.map(enrichRule), state = merge(baseState, { rules, - simulation: { config: { objectifs: ['startHere'] }, foldedSteps: [] } + simulation: { + defaultUnit: '€/an', + config: { objectifs: ['startHere'] }, + foldedSteps: [] + } }), currentQuestion = currentQuestionSelector(state) @@ -48,7 +52,11 @@ describe('conversation', function() { let step1 = merge(baseState, { rules, - simulation: { config: { objectifs: ['startHere'] }, foldedSteps: [] } + simulation: { + defaultUnit: '€/an', + config: { objectifs: ['startHere'] }, + foldedSteps: [] + } }) let step2 = reducers( assocPath(['simulation', 'situation'], { 'top . aa': '1' }, step1), @@ -125,7 +133,11 @@ describe('conversation', function() { let step1 = merge(baseState, { rules, - simulation: { config: { objectifs: ['net'] }, foldedSteps: [] } + simulation: { + defaultUnit: '€/an', + config: { objectifs: ['net'] }, + foldedSteps: [] + } }) expect(currentQuestionSelector(step1)).to.equal('brut') @@ -148,7 +160,11 @@ describe('real conversation', function() { it('should not have more than X questions', function() { let state = merge(baseState, { rules, - simulation: { config: salariéConfig, foldedSteps: [] } + simulation: { + defaultUnit: '€/an', + config: salariéConfig, + foldedSteps: [] + } }), nextSteps = nextStepsSelector(state) diff --git a/test/ficheDePaieSelector.test.js b/test/ficheDePaieSelector.test.js index ee94e05d5..2081fd3cc 100644 --- a/test/ficheDePaieSelector.test.js +++ b/test/ficheDePaieSelector.test.js @@ -10,6 +10,7 @@ import { let state = { rules, simulation: { + defaultUnit: '€/mois', config: salariéConfig, situation: { 'contrat salarié . rémunération . brut de base': '2300', diff --git a/test/generateQuestions.test.js b/test/generateQuestions.test.js index 4053b7b62..0f99293b7 100644 --- a/test/generateQuestions.test.js +++ b/test/generateQuestions.test.js @@ -237,8 +237,6 @@ describe('nextSteps', function() { analysis = analyse(rules, 'sum')(stateSelector), result = collectMissingVariables(analysis.targets) - // console.log('analysis', JSON.stringify(analysis, null, 4)) - expect(result).to.have.lengthOf(1) expect(result[0]).to.equal('top . sum . evt') }) diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml index 73178b473..140cfdd4d 100644 --- a/test/mécanismes/régularisation.yaml +++ b/test/mécanismes/régularisation.yaml @@ -10,6 +10,7 @@ plafond sécurité sociale: formule: 3500 €/mois | du 01/01/2020 | au 31/12/2020 retraite: + unité: €/mois formule: multiplication: assiette: salaire @@ -75,6 +76,7 @@ taux variable: - sinon: 20% cotisation spéciale: + unité: €/mois formule: régularisation: règle: From b43d3a859f76050f5f5d8cdc52b64cb1ebd33f17 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Wed, 18 Mar 2020 15:04:49 +0100 Subject: [PATCH 13/13] :white_check_mark: corrige les erreurs de type --- .../integration/mon-entreprise/simulateurs.js | 10 +- source/engine/evaluation.tsx | 14 +- source/engine/grammarFunctions.js | 2 +- source/engine/mecanisms/barème.ts | 10 +- source/engine/mecanisms/grille.ts | 8 +- source/engine/mecanisms/operation.js | 2 +- source/engine/mecanisms/régularisation.ts | 2 +- source/engine/mecanisms/trancheUtils.ts | 22 +- source/engine/mecanisms/variableTemporelle.ts | 4 +- source/engine/mecanisms/variations.ts | 39 +- source/engine/nodeUnits.ts | 4 +- source/engine/period.ts | 424 ------------------ source/engine/temporal.ts | 36 +- source/engine/units.ts | 9 +- source/locales/rules-en.yaml | 35 +- test/mécanismes/régularisation.yaml | 7 +- test/period.test.js | 2 +- 17 files changed, 141 insertions(+), 489 deletions(-) delete mode 100644 source/engine/period.ts diff --git a/cypress/integration/mon-entreprise/simulateurs.js b/cypress/integration/mon-entreprise/simulateurs.js index 5bd07caf1..79a105f71 100644 --- a/cypress/integration/mon-entreprise/simulateurs.js +++ b/cypress/integration/mon-entreprise/simulateurs.js @@ -47,10 +47,12 @@ describe('Simulateurs', function() { .first() .invoke('val') .should('match', /1[\s]000/) - cy.get(chargeInputSelector) - .first() - .invoke('val') - .should('be', '500') + if (['indépendant', 'assimilé-salarié'].includes(simulateur)) { + cy.get(chargeInputSelector) + .first() + .invoke('val') + .should('be', '500') + } }) it('should allow to navigate to a documentation page', function() { diff --git a/source/engine/evaluation.tsx b/source/engine/evaluation.tsx index ecfa9d32c..7e643d97f 100644 --- a/source/engine/evaluation.tsx +++ b/source/engine/evaluation.tsx @@ -13,12 +13,14 @@ import { typeWarning } from './error' import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits' import { concatTemporals, + EvaluatedNode, liftTemporalNode, mapTemporal, pureTemporal, + Temporal, temporalAverage, zipTemporals -} from './period' +} from './temporal' export let makeJsx = node => typeof node.jsx == 'function' @@ -139,8 +141,10 @@ export let parseObject = (recurse, objectShape, value) => { ) return value[key] != null ? recurse(value[key]) : defaultValue } - let transforms = fromPairs(map(k => [k, recurseOne(k)], keys(objectShape))) - return evolve(transforms, objectShape) + let transforms = fromPairs( + map(k => [k, recurseOne(k)], keys(objectShape)) as any + ) + return evolve(transforms as any, objectShape) } export let evaluateObject = (objectShape, effect) => ( @@ -170,7 +174,9 @@ export let evaluateObject = (objectShape, effect) => ( } }, temporalExplanations) - const sameUnitTemporalExplanation = convertNodesToSameUnit( + const sameUnitTemporalExplanation: Temporal> = convertNodesToSameUnit( temporalExplanation.map(x => x.value), cache._meta.contextRule, node.name diff --git a/source/engine/grammarFunctions.js b/source/engine/grammarFunctions.js index 0ba8986ad..442b6242d 100644 --- a/source/engine/grammarFunctions.js +++ b/source/engine/grammarFunctions.js @@ -2,7 +2,7 @@ The advantage of putting them here is to get prettier's JS formatting, since Nealrey doesn't support it https://github.com/kach/nearley/issues/310 */ import { normalizeDateString } from 'Engine/date' import { parseUnit } from 'Engine/units' -import { parsePeriod } from './period' +import { parsePeriod } from './temporal' export let binaryOperation = operationType => ([A, , operator, , B]) => ({ [operator]: { diff --git a/source/engine/mecanisms/barème.ts b/source/engine/mecanisms/barème.ts index 28e9721a9..663f3b269 100644 --- a/source/engine/mecanisms/barème.ts +++ b/source/engine/mecanisms/barème.ts @@ -3,8 +3,12 @@ import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation' import { decompose } from 'Engine/mecanisms/utils' import variations from 'Engine/mecanisms/variations' import Barème from 'Engine/mecanismViews/Barème' -import { liftTemporalNode, mapTemporal, temporalAverage } from 'Engine/period' -import { liftTemporal2 } from 'Engine/temporal' +import { + liftTemporal2, + liftTemporalNode, + mapTemporal, + temporalAverage +} from 'Engine/temporal' import { convertUnit } from '../units' import { parseUnit } from './../units' import { @@ -73,7 +77,7 @@ function evaluateBarème(tranches, assiette, evaluate, cache) { nodeValue: (Math.min(assiette.nodeValue, tranche.plafondValue) - tranche.plancherValue) * - convertUnit(taux.unit, parseUnit(''), taux.nodeValue) + convertUnit(taux.unit, parseUnit(''), taux.nodeValue as number) } }) } diff --git a/source/engine/mecanisms/grille.ts b/source/engine/mecanisms/grille.ts index 353e34c04..3c1fbfd4e 100644 --- a/source/engine/mecanisms/grille.ts +++ b/source/engine/mecanisms/grille.ts @@ -2,8 +2,12 @@ import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation' import { decompose } from 'Engine/mecanisms/utils' import variations from 'Engine/mecanisms/variations' import grille from 'Engine/mecanismViews/Grille' -import { liftTemporalNode, mapTemporal, temporalAverage } from 'Engine/period' -import { liftTemporal2 } from 'Engine/temporal' +import { + liftTemporal2, + liftTemporalNode, + mapTemporal, + temporalAverage +} from 'Engine/temporal' import { parseUnit } from 'Engine/units' import { lensPath, over } from 'ramda' import { diff --git a/source/engine/mecanisms/operation.js b/source/engine/mecanisms/operation.js index 1cbc2d200..8137e631b 100644 --- a/source/engine/mecanisms/operation.js +++ b/source/engine/mecanisms/operation.js @@ -3,7 +3,7 @@ import { typeWarning } from 'Engine/error' import { evaluateNode, makeJsx, mergeMissing } from 'Engine/evaluation' import { Node } from 'Engine/mecanismViews/common' import { convertNodeToUnit } from 'Engine/nodeUnits' -import { liftTemporal2, pureTemporal, temporalAverage } from 'Engine/period' +import { liftTemporal2, pureTemporal, temporalAverage } from 'Engine/temporal' import { inferUnit, serializeUnit } from 'Engine/units' import { curry, map } from 'ramda' import React from 'react' diff --git a/source/engine/mecanisms/régularisation.ts b/source/engine/mecanisms/régularisation.ts index b2d737ff5..8d9e74b86 100644 --- a/source/engine/mecanisms/régularisation.ts +++ b/source/engine/mecanisms/régularisation.ts @@ -9,7 +9,7 @@ import { Temporal, temporalAverage, temporalCumul -} from 'Engine/period' +} from 'Engine/temporal' import { Unit } from 'Engine/units' import { DottedName } from 'Types/rule' import { coerceArray } from '../../utils' diff --git a/source/engine/mecanisms/trancheUtils.ts b/source/engine/mecanisms/trancheUtils.ts index 6ebb1a85a..a0032d3af 100644 --- a/source/engine/mecanisms/trancheUtils.ts +++ b/source/engine/mecanisms/trancheUtils.ts @@ -1,4 +1,5 @@ import { mergeAllMissing } from 'Engine/evaluation' +import { Evaluation } from 'Engine/temporal' import { evolve } from 'ramda' import { evaluationError, typeWarning } from '../error' import { convertUnit, inferUnit } from '../units' @@ -41,19 +42,20 @@ export function evaluatePlafondUntilActiveTranche( ? tranches[i - 1].plafond : { nodeValue: 0 } - let plafondValue = + let plafondValue: Evaluation = plafond.nodeValue === null || multiplicateur.nodeValue === null ? null : plafond.nodeValue * multiplicateur.nodeValue try { - plafondValue = [Infinity || 0].includes(plafondValue) - ? plafondValue - : convertUnit( - inferUnit('*', [plafond.unit, multiplicateur.unit]), - assiette.unit, - plafondValue - ) + plafondValue = + plafondValue === Infinity || plafondValue === 0 + ? plafondValue + : convertUnit( + inferUnit('*', [plafond.unit, multiplicateur.unit]), + assiette.unit, + plafondValue + ) } catch (e) { typeWarning( cache._meta.contextRule, @@ -91,7 +93,7 @@ export function evaluatePlafondUntilActiveTranche( if ( !!tranches[i - 1] && !!plancherValue && - plafondValue <= plancherValue + plafondValue <= plancherValue ) { evaluationError( cache._meta.contextRule, @@ -108,7 +110,7 @@ export function evaluatePlafondUntilActiveTranche( isAfterActive, isActive: assiette.nodeValue >= plancherValue && - assiette.nodeValue < plafondValue + assiette.nodeValue < plafondValue } return [[...tranches, tranche], tranche.isActive] diff --git a/source/engine/mecanisms/variableTemporelle.ts b/source/engine/mecanisms/variableTemporelle.ts index 194d3b943..80f09383b 100644 --- a/source/engine/mecanisms/variableTemporelle.ts +++ b/source/engine/mecanisms/variableTemporelle.ts @@ -3,8 +3,8 @@ import { createTemporalEvaluation, narrowTemporalValue, temporalAverage -} from 'Engine/period' -import { Temporal } from './../period' +} from 'Engine/temporal' +import { Temporal } from './../temporal' function evaluate( cache: any, diff --git a/source/engine/mecanisms/variations.ts b/source/engine/mecanisms/variations.ts index 2b06b8cb9..83aa46265 100644 --- a/source/engine/mecanisms/variations.ts +++ b/source/engine/mecanisms/variations.ts @@ -7,9 +7,9 @@ import { pureTemporal, sometime, temporalAverage -} from 'Engine/period' +} from 'Engine/temporal' import { inferUnit } from 'Engine/units' -import { dissoc, or } from 'ramda' +import { or } from 'ramda' import { mergeAllMissing } from './../evaluation' /* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */ @@ -36,18 +36,29 @@ export default function parse(recurse, k, v, devariate) { ) } } - -export let devariateExplanation = (recurse, mecanismKey, v) => { - let fixedProps = dissoc('variations')(v), - explanation = v.variations.map(({ si, alors, sinon }) => ({ - consequence: recurse({ - [mecanismKey]: { - ...fixedProps, - ...(sinon || alors) - } - }), - condition: sinon ? defaultNode(true) : recurse(si) - })) +type Variation = + | { + si: any + alors: Object + } + | { + sinon: Object + } +export let devariateExplanation = ( + recurse, + mecanismKey, + v: { variations: Array } +) => { + const { variations, ...fixedProps } = v + const explanation = variations.map(variation => ({ + condition: 'sinon' in variation ? defaultNode(true) : recurse(variation.si), + consequence: recurse({ + [mecanismKey]: { + ...fixedProps, + ...('sinon' in variation ? variation.sinon : variation.alors) + } + }) + })) return explanation } diff --git a/source/engine/nodeUnits.ts b/source/engine/nodeUnits.ts index c56fd7728..b41e669e7 100644 --- a/source/engine/nodeUnits.ts +++ b/source/engine/nodeUnits.ts @@ -1,4 +1,4 @@ -import { mapTemporal } from './period' +import { EvaluatedNode, mapTemporal } from './temporal' import { areUnitConvertible, convertUnit, @@ -37,7 +37,7 @@ export const getNodeDefaultUnit = (node, cache) => { ) } -export function convertNodeToUnit(to: Unit, node) { +export function convertNodeToUnit(to: Unit, node: EvaluatedNode) { return { ...node, nodeValue: node.unit diff --git a/source/engine/period.ts b/source/engine/period.ts deleted file mode 100644 index cee75af2e..000000000 --- a/source/engine/period.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { - convertToDate, - getDifferenceInDays, - getDifferenceInMonths, - getDifferenceInYears, - getRelativeDate, - getYear -} from 'Engine/date' -import { Unit } from './units' - -export type Period = { - start: T | null - end: T | null -} - -export function parsePeriod(word: string, date: Date): Period { - const startWords = [ - 'depuis', - 'depuis le', - 'depuis la', - 'à partir de', - 'à partir du', - 'du' - ] - const endWords = [ - "jusqu'à", - "jusqu'au", - "jusqu'à la", - 'avant', - 'avant le', - 'avant la', - 'au' - ] - const intervalWords = ['le', 'en'] - if (!startWords.concat(endWords, intervalWords).includes(word)) { - throw new SyntaxError( - `Le mot clé '${word}' n'est pas valide. Les mots clés possible sont les suivants :\n\t ${startWords.join( - ', ' - )}` - ) - } - if (word === 'le') { - return { - start: date, - end: date - } - } - if (word === 'en') { - return { start: null, end: null } - } - if (startWords.includes(word)) { - return { - start: date, - end: null - } - } - if (endWords.includes(word)) { - return { - start: null, - end: date - } - } - throw new Error('Non implémenté') -} - -// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable) -// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)] -export type Evaluation = T | false | null - -export type EvaluatedNode = { - nodeValue: Evaluation - temporalValue?: Temporal> -} - -export type TemporalNode = Temporal<{ nodeValue: Evaluation }> -export type Temporal = Array & { value: T }> - -export function narrowTemporalValue( - period: Period, - temporalValue: Temporal> -): Temporal> { - return liftTemporal2( - (value, filter) => filter && value, - temporalValue, - createTemporalEvaluation(true, period) - ) -} - -// Returns a temporal value that's true for the given period and false otherwise. -export function createTemporalEvaluation( - value: Evaluation, - period: Period = { start: null, end: null } -): Temporal> { - let temporalValue = [{ ...period, value }] - if (period.start != null) { - temporalValue.unshift({ - start: null, - end: getRelativeDate(period.start, -1), - value: false - }) - } - if (period.end != null) { - temporalValue.push({ - start: getRelativeDate(period.end, 1), - end: null, - value: false - }) - } - return temporalValue -} - -export function pureTemporal(value: T): Temporal { - return [{ start: null, end: null, value }] -} - -export function mapTemporal( - fn: (value: T1) => T2, - temporalValue: Temporal -): Temporal { - return temporalValue.map(({ start, end, value }) => ({ - start, - end, - value: fn(value) - })) -} -export function sometime( - fn: (value: T1) => boolean, - temporalValue: Temporal -): boolean { - return temporalValue.some(({ start, end, value }) => fn(value)) -} - -export function liftTemporal2( - fn: (value1: T1, value2: T2) => T3, - temporalValue1: Temporal, - temporalValue2: Temporal -): Temporal { - return mapTemporal( - ([a, b]) => fn(a, b), - zipTemporals(temporalValue1, temporalValue2) - ) -} - -export function concatTemporals( - temporalValues: Array> -): Temporal> { - return temporalValues.reduce( - (values, value) => liftTemporal2((a, b) => [...a, b], values, value), - pureTemporal([]) as Temporal> - ) -} - -export function liftTemporalNode(node: EvaluatedNode): TemporalNode { - const { temporalValue, ...baseNode } = node - if (!temporalValue) { - return pureTemporal(baseNode) - } - return mapTemporal( - nodeValue => ({ - ...baseNode, - nodeValue - }), - temporalValue - ) -} - -export function zipTemporals( - temporalValue1: Temporal, - temporalValue2: Temporal, - acc: Temporal<[T1, T2]> = [] -): Temporal<[T1, T2]> { - if (!temporalValue1.length && !temporalValue2.length) { - return acc - } - const [value1, ...rest1] = temporalValue1 - const [value2, ...rest2] = temporalValue2 - console.assert(value1.start === value2.start) - const endDateComparison = compareEndDate(value1.end, value2.end) - - // End dates are equals - if (endDateComparison === 0) { - return zipTemporals(rest1, rest2, [ - ...acc, - { ...value1, value: [value1.value, value2.value] } - ]) - } - // Value1 lasts longuer than value1 - if (endDateComparison > 0) { - console.assert(value2.end !== null) - return zipTemporals( - [ - { ...value1, start: getRelativeDate(value2.end as string, 1) }, - ...rest1 - ], - rest2, - [ - ...acc, - { - ...value2, - value: [value1.value, value2.value] - } - ] - ) - } - - // Value2 lasts longuer than value1 - if (endDateComparison < 0) { - console.assert(value1.end !== null) - return zipTemporals( - rest1, - [ - { ...value2, start: getRelativeDate(value1.end as string, 1) }, - ...rest2 - ], - [ - ...acc, - { - ...value1, - value: [value1.value, value2.value] - } - ] - ) - } - throw new EvalError('All case should have been covered') -} - -function beginningOfNextYear(date: string): string { - return `01/01/${getYear(date) + 1}` -} - -function endsOfPreviousYear(date: string): string { - return `31/12/${getYear(date) - 1}` -} - -function splitStartsAt( - fn: (date: string) => string, - temporal: Temporal -): Temporal { - return temporal.reduce((acc, period) => { - const { start, end } = period - const newStart = start === null ? start : fn(start) - if (compareEndDate(newStart, end) !== -1) { - return [...acc, period] - } - console.assert(newStart !== null) - return [ - ...acc, - { ...period, end: getRelativeDate(newStart as string, -1) }, - { ...period, start: newStart } - ] - }, [] as Temporal) -} - -function splitEndsAt( - fn: (date: string) => string, - temporal: Temporal -): Temporal { - return temporal.reduce((acc, period) => { - const { start, end } = period - const newEnd = end === null ? end : fn(end) - if (compareStartDate(start, newEnd) !== -1) { - return [...acc, period] - } - console.assert(newEnd !== null) - return [ - ...acc, - { ...period, end: newEnd }, - { ...period, start: getRelativeDate(newEnd as string, 1) } - ] - }, [] as Temporal) -} - -export function groupByYear(temporalValue: Temporal): Array> { - return ( - // First step: split period by year if needed - splitEndsAt( - endsOfPreviousYear, - splitStartsAt(beginningOfNextYear, temporalValue) - ) - // Second step: group period by year - .reduce((acc, period) => { - const [currentTemporal, ...otherTemporal] = acc - if (currentTemporal === undefined) { - return [[period]] - } - const firstPeriod = currentTemporal[0] - console.assert( - firstPeriod !== undefined && - firstPeriod.end !== null && - period.start !== null, - 'invariant non verifié' - ) - if ( - (firstPeriod.end as string).slice(-4) !== - (period.start as string).slice(-4) - ) { - return [[period], ...acc] - } - return [[...currentTemporal, period], ...otherTemporal] - }, [] as Array>) - .reverse() - ) -} - -function simplify(temporalValue: Temporal): Temporal { - return temporalValue -} - -function compareStartDate( - dateA: string | null, - dateB: string | null -): -1 | 0 | 1 { - if (dateA == dateB) { - return 0 - } - if (dateA == null) { - return -1 - } - if (dateB == null) { - return 1 - } - return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1 -} - -function compareEndDate( - dateA: string | null, - dateB: string | null -): -1 | 0 | 1 { - if (dateA == dateB) { - return 0 - } - if (dateA == null) { - return 1 - } - if (dateB == null) { - return -1 - } - return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1 -} - -export function temporalAverage( - temporalValue: Temporal>, - unit?: Unit -): Evaluation { - temporalValue = temporalValue.filter(({ value }) => value !== false) - const first = temporalValue[0] - const last = temporalValue[temporalValue.length - 1] - if (!temporalValue.length) { - return false - } - if (temporalValue.length === 1) { - return temporalValue[0].value - } - - // La variable est définie sur un interval infini - if (first.start == null || last.end == null) { - if (first.start != null) { - return last.value - } - if (last.end != null) { - return first.value - } - return (first.value + last.value) / 2 - } - - if (temporalValue.some(({ value }) => value == null)) { - return null - } - let totalWeight = 0 - const weights = temporalValue.map(({ start, end, value }) => { - let weight = 0 - if (unit?.denominators.includes('mois')) { - weight = getDifferenceInMonths(start, end) - } else if (unit?.denominators.includes('année')) { - weight = getDifferenceInYears(start, end) - } else { - weight = getDifferenceInDays(start, end) - } - totalWeight += weight - return (value as number) * weight - }) - return weights.reduce( - (average, weightedValue) => average + weightedValue / totalWeight, - 0 - ) -} - -export function temporalCumul( - temporalValue: Temporal>, - unit: Unit -): Evaluation { - temporalValue = temporalValue.filter(({ value }) => value !== false) - const first = temporalValue[0] - const last = temporalValue[temporalValue.length - 1] - if (!temporalValue.length) { - return false - } - - // La variable est définie sur un interval infini - if (first.start == null || last.end == null) { - if (first.start != null) { - return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity - } - if (last.end != null) { - return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity - } - return null - } - if (temporalValue.some(({ value }) => value == null)) { - return null - } - - return temporalValue.reduce((acc, { start, end, value }) => { - let weight = 1 - if (unit?.denominators.includes('mois')) { - weight = getDifferenceInMonths(start, end) - } else if (unit?.denominators.includes('année')) { - weight = getDifferenceInYears(start, end) - } else if (unit?.denominators.includes('jour')) { - weight = getDifferenceInDays(start, end) - } - return value * weight + acc - }, 0) -} diff --git a/source/engine/temporal.ts b/source/engine/temporal.ts index cee75af2e..f307198e3 100644 --- a/source/engine/temporal.ts +++ b/source/engine/temporal.ts @@ -68,8 +68,11 @@ export function parsePeriod(word: string, date: Date): 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 }> @@ -343,8 +346,6 @@ export function temporalAverage( unit?: Unit ): Evaluation { temporalValue = temporalValue.filter(({ value }) => value !== false) - const first = temporalValue[0] - const last = temporalValue[temporalValue.length - 1] if (!temporalValue.length) { return false } @@ -352,6 +353,14 @@ export function temporalAverage( return temporalValue[0].value } + if (temporalValue.some(({ value }) => value == null)) { + return null + } + + const temporalNumber = temporalValue as Temporal + const first = temporalNumber[0] + const last = temporalNumber[temporalNumber.length - 1] + // La variable est définie sur un interval infini if (first.start == null || last.end == null) { if (first.start != null) { @@ -363,11 +372,9 @@ export function temporalAverage( return (first.value + last.value) / 2 } - if (temporalValue.some(({ value }) => value == null)) { - return null - } let totalWeight = 0 - const weights = temporalValue.map(({ start, end, value }) => { + const weights = temporalNumber.map(({ start, end, value }) => { + ;[start, end] = [start, end] as [string, string] let weight = 0 if (unit?.denominators.includes('mois')) { weight = getDifferenceInMonths(start, end) @@ -377,7 +384,7 @@ export function temporalAverage( weight = getDifferenceInDays(start, end) } totalWeight += weight - return (value as number) * weight + return value * weight }) return weights.reduce( (average, weightedValue) => average + weightedValue / totalWeight, @@ -390,12 +397,18 @@ export function temporalCumul( unit: Unit ): Evaluation { temporalValue = temporalValue.filter(({ value }) => value !== false) - const first = temporalValue[0] - const last = temporalValue[temporalValue.length - 1] if (!temporalValue.length) { return false } + if (temporalValue.some(({ value }) => value == null)) { + return null + } + + const temporalNumber = temporalValue as Temporal + const first = temporalNumber[0] + const last = temporalNumber[temporalNumber.length - 1] + // La variable est définie sur un interval infini if (first.start == null || last.end == null) { if (first.start != null) { @@ -406,11 +419,12 @@ export function temporalCumul( } return null } - if (temporalValue.some(({ value }) => value == null)) { + if (temporalNumber.some(({ value }) => value == null)) { return null } - return temporalValue.reduce((acc, { start, end, value }) => { + return temporalNumber.reduce((acc, { start, end, value }) => { + ;[start, end] = [start, end] as [string, string] let weight = 1 if (unit?.denominators.includes('mois')) { weight = getDifferenceInMonths(start, end) diff --git a/source/engine/units.ts b/source/engine/units.ts index 0d40601c9..f6b146774 100644 --- a/source/engine/units.ts +++ b/source/engine/units.ts @@ -12,6 +12,7 @@ import { without } from 'ramda' import i18n from '../i18n' +import { Evaluation } from './temporal' type BaseUnit = string @@ -186,7 +187,13 @@ export function convertUnit( from: Unit | undefined, to: Unit | undefined, value: number -) { +): number +export function convertUnit( + from: Unit | undefined, + to: Unit | undefined, + value: Evaluation +): Evaluation +export function convertUnit(from, to, value): any { if (!areUnitConvertible(from, to)) { throw new Error( `Impossible de convertir l'unité '${serializeUnit( diff --git a/source/locales/rules-en.yaml b/source/locales/rules-en.yaml index 3f38fc666..02ba489d6 100644 --- a/source/locales/rules-en.yaml +++ b/source/locales/rules-en.yaml @@ -4207,8 +4207,39 @@ dirigeant . indépendant . revenu net de cotisations: titre.en: net contribution income titre.fr: revenu net de cotisations dirigeant . indépendant . revenu professionnel: - titre.en: '[automatic] occupational income' - titre.fr: revenu professionnel + description.en: > + [automatic] This is the net deductible contribution income of the + self-employed person, which is used as the basis for the calculation of + contributions and tax for self-employed persons. + + + Attention, **our calculation is made at cruising speed**: + + the self-employed person who starts out will pay a relatively small package + of social security contributions for the first 2 years. He will then have to + regularise this situation in relation to the income he has actually + received. + + + Therefore, this calculation should be seen as "the amount that will have to + be paid* in the short term after 2 years of exercise. + description.fr: > + C'est le revenu net de cotisations déductibles du travailleur indépendant, + qui sert de base au calcul des cotisations et de l'impôt pour les + indépendants. + + + Attention, **notre calcul est fait au régime de croisière**: + + l'indépendant qui se lance paiera pendant ses 2 premières années un forfait + relativement réduit de cotisations sociales. Il devra ensuite régulariser + cette situation par rapport au revenu qu'il a vraiment perçu. + + + Il faut donc voir ce calcul comme *le montant qui devra de toute façon être + payé* à court terme après 2 ans d'exercice. + titre.en: '[automatic] professional income (net taxable)' + titre.fr: revenu professionnel (net imposable) dirigeant . indépendant . revenus étrangers: description.en: > [automatic] Foreign income is income declared by self-employed persons in diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml index 140cfdd4d..4645c1859 100644 --- a/test/mécanismes/régularisation.yaml +++ b/test/mécanismes/régularisation.yaml @@ -20,12 +20,7 @@ retraite: retraite . avec régularisation: formule: régularisation: - règle: - multiplication: - assiette: salaire - plafond: plafond sécurité sociale - taux: 10% - + règle: retraite valeurs cumulées: - salaire - plafond sécurité sociale diff --git a/test/period.test.js b/test/period.test.js index 416c82aec..4316b3869 100644 --- a/test/period.test.js +++ b/test/period.test.js @@ -4,7 +4,7 @@ import { createTemporalEvaluation, groupByYear, zipTemporals -} from '../source/engine/period' +} from '../source/engine/temporal' const neverEnding = value => [{ start: null, end: null, value: value }] describe('Periods : zip', () => {