From fcb44fc317957f05047f3166c57bc5ca9fdf39f3 Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Thu, 15 Oct 2020 19:09:41 +0200 Subject: [PATCH] =?UTF-8?q?:sparkles:=20transforme=20applicable=20si=20et?= =?UTF-8?q?=20non=20applicable=20si=20en=20m=C3=A9canisme=20chain=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Par la même occasion, uniformise l'écriture des mécanismes chainées --- .../source/components/Feedback/LinkToForm.tsx | 1 - .../components/conversation/Conversation.tsx | 1 - mon-entreprise/source/rules/dirigeant.yaml | 27 ++--- mon-entreprise/source/rules/impôt.yaml | 3 +- .../__snapshots__/simulations.jest.js.snap | 4 +- .../source/components/mecanisms/Recalcul.tsx | 1 - .../source/components/mecanisms/common.tsx | 5 +- publicodes/source/cyclesLib/ASTTypes.ts | 78 +++++++++++-- .../source/cyclesLib/rulesDependencies.ts | 46 ++++++-- publicodes/source/evaluation.tsx | 2 +- publicodes/source/mecanisms/applicable.tsx | 58 ++++++++++ publicodes/source/mecanisms/arrondi.tsx | 107 +++++++----------- publicodes/source/mecanisms/durée.tsx | 7 +- publicodes/source/mecanisms/encadrement.tsx | 97 ---------------- publicodes/source/mecanisms/group.tsx | 66 ----------- publicodes/source/mecanisms/nonApplicable.tsx | 66 +++++++++++ publicodes/source/mecanisms/operation.tsx | 7 +- publicodes/source/mecanisms/plafond.tsx | 82 ++++++++++++++ publicodes/source/mecanisms/plancher.tsx | 81 +++++++++++++ publicodes/source/mecanisms/product.tsx | 6 +- publicodes/source/parse.tsx | 90 ++++++++------- publicodes/source/parseRules.ts | 15 ++- publicodes/test/mecanisms.test.js | 1 + publicodes/test/mécanismes/applicable.yaml | 13 +++ publicodes/test/mécanismes/arrondi.yaml | 11 +- publicodes/test/mécanismes/encadrement.yaml | 31 +++-- .../test/mécanismes/paramètres-nommés.yaml | 6 +- publicodes/test/mécanismes/recalcul.yaml | 11 +- 28 files changed, 558 insertions(+), 365 deletions(-) create mode 100644 publicodes/source/mecanisms/applicable.tsx delete mode 100644 publicodes/source/mecanisms/encadrement.tsx delete mode 100644 publicodes/source/mecanisms/group.tsx create mode 100644 publicodes/source/mecanisms/nonApplicable.tsx create mode 100644 publicodes/source/mecanisms/plafond.tsx create mode 100644 publicodes/source/mecanisms/plancher.tsx diff --git a/mon-entreprise/source/components/Feedback/LinkToForm.tsx b/mon-entreprise/source/components/Feedback/LinkToForm.tsx index d2f12bcb1..79638cf95 100644 --- a/mon-entreprise/source/components/Feedback/LinkToForm.tsx +++ b/mon-entreprise/source/components/Feedback/LinkToForm.tsx @@ -7,7 +7,6 @@ export default function LinkToForm() { document.referrer || document.location.origin ).hostname.replace(/^www\.|^m\./, '') - console.log(hostname) return (
dispatch( validateStepWithValue( diff --git a/mon-entreprise/source/rules/dirigeant.yaml b/mon-entreprise/source/rules/dirigeant.yaml index cd1e27efb..7365ebf4c 100644 --- a/mon-entreprise/source/rules/dirigeant.yaml +++ b/mon-entreprise/source/rules/dirigeant.yaml @@ -718,9 +718,8 @@ dirigeant . indépendant . conjoint collaborateur . cotisations . indemnités jo unité: €/an formule: produit: - assiette: plafond sécurité sociale temps plein - facteur: 40% - taux: 0.85% + assiette: 40% * plafond sécurité sociale temps plein + taux: cotisations et contributions . indemnités journalières maladie . taux arrondi: oui dirigeant . indépendant . cotisations et contributions . cotisations: @@ -904,7 +903,7 @@ dirigeant . indépendant . cotisations et contributions . indemnités journaliè formule: produit: assiette: maladie . assiette - taux: 0.85% + taux [ref]: 0.85% plafond: 5 * plafond sécurité sociale temps plein arrondi: oui @@ -912,9 +911,8 @@ dirigeant . indépendant . cotisations et contributions . maladie: formule: barème: assiette [ref]: - encadrement: - valeur: assiette des cotisations - plancher [ref]: 40% * plafond sécurité sociale temps plein + valeur: assiette des cotisations + plancher [ref]: 40% * plafond sécurité sociale temps plein multiplicateur: plafond sécurité sociale temps plein tranches: - taux: taux variable @@ -940,9 +938,8 @@ dirigeant . indépendant . cotisations et contributions . maladie . taux variabl dirigeant . indépendant . cotisations et contributions . maladie . taux RSA: formule: - encadrement: - valeur: taux RSA part variable + 1.35% - plafond: 6.35% + valeur: taux RSA part variable + 1.35% + plafond: 6.35% note: | Pour les indépendants au RSA, seule la réduction simple définie dans le décret de calcul de la cotisation maladie est prise en compte. La réduction renforcée en-dessous de 40% du plafond de la sécurité sociale ne l'est pas, car il n'y a pas d'assiette minimale. @@ -977,9 +974,8 @@ dirigeant . indépendant . cotisations et contributions . retraite de base: formule: barème: assiette [ref]: - encadrement: - valeur: assiette des cotisations - plancher [ref]: 11.5% * plafond sécurité sociale temps plein + valeur: assiette des cotisations + plancher [ref]: 11.5% * plafond sécurité sociale temps plein multiplicateur: plafond sécurité sociale temps plein tranches: - taux: 17.75% @@ -1009,9 +1005,8 @@ dirigeant . indépendant . cotisations et contributions . invalidité et décès formule: produit: assiette [ref]: - encadrement: - valeur: assiette des cotisations - plancher [ref]: 11.5% * plafond sécurité sociale temps plein + valeur: assiette des cotisations + plancher [ref]: 11.5% * plafond sécurité sociale temps plein taux: 1.3% plafond: plafond sécurité sociale temps plein arrondi: oui diff --git a/mon-entreprise/source/rules/impôt.yaml b/mon-entreprise/source/rules/impôt.yaml index 23bcde586..4650e0873 100644 --- a/mon-entreprise/source/rules/impôt.yaml +++ b/mon-entreprise/source/rules/impôt.yaml @@ -89,7 +89,8 @@ impôt . revenu imposable . abattement contrat court: - contrat salarié . CDD - contrat salarié . CDD . durée contrat <= 2 mois formule: - arrondi: 50% * SMIC temps plein . net imposable * 1 mois + valeur: 50% * SMIC temps plein . net imposable * 1 mois + arrondi: oui note: Cet abattement s'applique aussi pour les conventions de stage ou les contrats de mission (intérim) de moins de 2 mois. références: Bofip - dispositions spécifiques aux contrats courts: https://bofip.impots.gouv.fr/bofip/11252-PGP.html?identifiant=BOI-IR-PAS-20-20-30-10-20180515 diff --git a/mon-entreprise/test/regressions/__snapshots__/simulations.jest.js.snap b/mon-entreprise/test/regressions/__snapshots__/simulations.jest.js.snap index cdfeb09b0..2324cac53 100644 --- a/mon-entreprise/test/regressions/__snapshots__/simulations.jest.js.snap +++ b/mon-entreprise/test/regressions/__snapshots__/simulations.jest.js.snap @@ -196,7 +196,7 @@ exports[`calculate simulations-professions-libérales: auxiliaire médical 1`] = exports[`calculate simulations-professions-libérales: auxiliaire médical 2`] = `"[30000,0,8077,21923,932,20991]"`; -exports[`calculate simulations-professions-libérales: auxiliaire médical 3`] = `"[300000,0,61784,238216,81297,156919]"`; +exports[`calculate simulations-professions-libérales: auxiliaire médical 3`] = `"[300000,0,61784,238217,81297,156920]"`; exports[`calculate simulations-professions-libérales: avocat 1`] = `"[50000,0,11410,38590,4753,33837]"`; @@ -210,7 +210,7 @@ exports[`calculate simulations-professions-libérales: médecin 1`] = `"[50000,0 exports[`calculate simulations-professions-libérales: médecin 2`] = `"[50000,0,20229,29771,2334,27437]"`; -exports[`calculate simulations-professions-libérales: médecin 3`] = `"[300000,0,86481,213519,73147,140372]"`; +exports[`calculate simulations-professions-libérales: médecin 3`] = `"[300000,0,86481,213520,73147,140373]"`; exports[`calculate simulations-professions-libérales: médecin 4`] = `"[400000,0,106201,293799,115768,178031]"`; diff --git a/publicodes/source/components/mecanisms/Recalcul.tsx b/publicodes/source/components/mecanisms/Recalcul.tsx index c95939d51..204c69f5f 100644 --- a/publicodes/source/components/mecanisms/Recalcul.tsx +++ b/publicodes/source/components/mecanisms/Recalcul.tsx @@ -5,7 +5,6 @@ import { Trans } from 'react-i18next' import { Mecanism } from './common' export default function Recalcul({ nodeValue, explanation, unit }) { - console.log(nodeValue, explanation) return ( <> diff --git a/publicodes/source/components/mecanisms/common.tsx b/publicodes/source/components/mecanisms/common.tsx index 5ac6c7045..fd21f529f 100644 --- a/publicodes/source/components/mecanisms/common.tsx +++ b/publicodes/source/components/mecanisms/common.tsx @@ -102,10 +102,12 @@ export function Mecanism({ export const InfixMecanism = ({ value, + prefixed, children }: { value: EvaluatedNode children: React.ReactNode + prefixed?: boolean }) => { return (
+ {prefixed && children}
{makeJsx(value)}
- {children} + {!prefixed && children}
) } diff --git a/publicodes/source/cyclesLib/ASTTypes.ts b/publicodes/source/cyclesLib/ASTTypes.ts index 371d4d719..eeb763f00 100644 --- a/publicodes/source/cyclesLib/ASTTypes.ts +++ b/publicodes/source/cyclesLib/ASTTypes.ts @@ -167,26 +167,78 @@ export function isRecalculMech( ) } -export type EncadrementMech = AbstractMechanism & { - name: 'encadrement' +export type PlafondMech = AbstractMechanism & { + name: 'plafond' explanation: { valeur: ASTNode plafond: ASTNode + } +} +export function isPlafondMech(node: ASTNode): node is PlafondMech { + const encadrementMech = node as PlafondMech + return ( + isAbstractMechanism(encadrementMech) && + encadrementMech.name == 'plafond' && + typeof encadrementMech.explanation === 'object' && + encadrementMech.explanation.valeur !== undefined && + encadrementMech.explanation.plafond !== undefined + ) +} + +export type PlancherMech = AbstractMechanism & { + name: 'plancher' + explanation: { + valeur: ASTNode plancher: ASTNode } } -export function isEncadrementMech(node: ASTNode): node is EncadrementMech { - const encadrementMech = node as EncadrementMech +export function isPlancherMech(node: ASTNode): node is PlancherMech { + const encadrementMech = node as PlancherMech return ( isAbstractMechanism(encadrementMech) && - encadrementMech.name == 'encadrement' && + encadrementMech.name == 'plancher' && typeof encadrementMech.explanation === 'object' && encadrementMech.explanation.valeur !== undefined && - encadrementMech.explanation.plafond !== undefined && encadrementMech.explanation.plancher !== undefined ) } +export type ApplicableMech = AbstractMechanism & { + name: 'applicable si' + explanation: { + valeur: ASTNode + condition: ASTNode + } +} +export function isApplicableMech(node: ASTNode): node is ApplicableMech { + const mech = node as ApplicableMech + return ( + isAbstractMechanism(mech) && + mech.name == 'applicable si' && + typeof mech.explanation === 'object' && + mech.explanation.valeur !== undefined && + mech.explanation.condition !== undefined + ) +} + +export type NonApplicableMech = AbstractMechanism & { + name: 'non applicable si' + explanation: { + valeur: ASTNode + condition: ASTNode + } +} +export function isNonApplicableMech(node: ASTNode): node is NonApplicableMech { + const mech = node as NonApplicableMech + return ( + isAbstractMechanism(mech) && + mech.name == 'non applicable si' && + typeof mech.explanation === 'object' && + mech.explanation.valeur !== undefined && + mech.explanation.condition !== undefined + ) +} + export type SommeMech = AbstractMechanism & { name: 'somme' explanation: Array @@ -325,8 +377,8 @@ export function isArrondiMech(node: ASTNode): node is ArrondiMech { isAbstractMechanism(arrondiMech) && arrondiMech.name === 'arrondi' && typeof arrondiMech.explanation === 'object' && - arrondiMech.explanation.decimals !== undefined && - arrondiMech.explanation.value !== undefined + arrondiMech.explanation.arrondi !== undefined && + arrondiMech.explanation.valeur !== undefined ) } @@ -484,7 +536,10 @@ export function isDureeMech(node: ASTNode): node is DureeMech { export type AnyMechanism = | RecalculMech - | EncadrementMech + | PlancherMech + | PlafondMech + | ApplicableMech + | NonApplicableMech | SommeMech | ProduitMech | VariationsMech @@ -506,7 +561,10 @@ export function isAnyMechanism( ): node is AnyMechanism { return ( isRecalculMech(node) || - isEncadrementMech(node) || + isPlafondMech(node) || + isPlancherMech(node) || + isApplicableMech(node) || + isNonApplicableMech(node) || isSommeMech(node) || isProduitMech(node) || isVariationsMech(node) || diff --git a/publicodes/source/cyclesLib/rulesDependencies.ts b/publicodes/source/cyclesLib/rulesDependencies.ts index b73411fc6..623d24877 100644 --- a/publicodes/source/cyclesLib/rulesDependencies.ts +++ b/publicodes/source/cyclesLib/rulesDependencies.ts @@ -56,12 +56,11 @@ export function ruleDepsOfNode( return ruleReference === ruleName ? [] : [ruleReference] } - function ruleDepsOfEncadrementMech( - encadrementMech: ASTTypes.EncadrementMech + function ruleDepsOfPlafondMech( + encadrementMech: ASTTypes.PlafondMech ): RuleDependencies { const result = [ encadrementMech.explanation.plafond, - encadrementMech.explanation.plancher, encadrementMech.explanation.valeur ].flatMap( R.partial>( @@ -72,6 +71,33 @@ export function ruleDepsOfNode( return result } + function ruleDepsOfPlancherMech( + mech: ASTTypes.PlancherMech + ): RuleDependencies { + const result = [mech.explanation.plancher, mech.explanation.valeur].flatMap( + R.partial>( + ruleDepsOfNode, + [ruleName] + ) + ) + return result + } + + function ruleDepsOfApplicableMech( + mech: ASTTypes.ApplicableMech | ASTTypes.NonApplicableMech + ): RuleDependencies { + const result = [ + mech.explanation.condition, + mech.explanation.valeur + ].flatMap( + R.partial>( + ruleDepsOfNode, + [ruleName] + ) + ) + return result + } + function ruleDepsOfSommeMech( sommeMech: ASTTypes.SommeMech ): RuleDependencies { @@ -168,8 +194,8 @@ export function ruleDepsOfNode( arrondiMech: ASTTypes.ArrondiMech ): RuleDependencies { const result = [ - arrondiMech.explanation.decimals, - arrondiMech.explanation.value + arrondiMech.explanation.arrondi, + arrondiMech.explanation.valeur ].flatMap( R.partial>( ruleDepsOfNode, @@ -312,8 +338,14 @@ export function ruleDepsOfNode( result = ruleDepsOfPossibilities2(node) } else if (ASTTypes.isRecalculMech(node)) { result = ruleDepsOfRecalculMech(node) - } else if (ASTTypes.isEncadrementMech(node)) { - result = ruleDepsOfEncadrementMech(node) + } else if (ASTTypes.isApplicableMech(node)) { + result = ruleDepsOfApplicableMech(node) + } else if (ASTTypes.isNonApplicableMech(node)) { + result = ruleDepsOfApplicableMech(node) + } else if (ASTTypes.isPlafondMech(node)) { + result = ruleDepsOfPlafondMech(node) + } else if (ASTTypes.isPlancherMech(node)) { + result = ruleDepsOfPlancherMech(node) } else if (ASTTypes.isSommeMech(node)) { result = ruleDepsOfSommeMech(node) } else if (ASTTypes.isProduitMech(node)) { diff --git a/publicodes/source/evaluation.tsx b/publicodes/source/evaluation.tsx index 65daba6ba..b4cb8d5d2 100644 --- a/publicodes/source/evaluation.tsx +++ b/publicodes/source/evaluation.tsx @@ -33,7 +33,7 @@ export const collectNodeMissing = node => node.missingVariables || {} export const bonus = (missings, hasCondition = true) => hasCondition ? map(x => x + 0.0001, missings || {}) : missings export const mergeAllMissing = missings => - reduce(mergeWith(add), {}, map(collectNodeMissing, missings)) + missings.map(collectNodeMissing).reduce(mergeMissing, {}) export const mergeMissing = (left, right) => mergeWith(add, left || {}, right || {}) diff --git a/publicodes/source/mecanisms/applicable.tsx b/publicodes/source/mecanisms/applicable.tsx new file mode 100644 index 000000000..fd43597e9 --- /dev/null +++ b/publicodes/source/mecanisms/applicable.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { InfixMecanism } from '../components/mecanisms/common' +import { bonus, evaluateNode, makeJsx, mergeMissing } from '../evaluation' + +function MecanismApplicable({ explanation }) { + return ( + +

+ Applicable si : + {makeJsx(explanation.applicable)} +

+
+ ) +} + +const evaluate = (cache, situation, parsedRules, node) => { + const evaluateAttribute = evaluateNode.bind( + null, + cache, + situation, + parsedRules + ) + const condition = evaluateAttribute(node.explanation.condition) + let valeur = node.explanation.valeur + if (condition.nodeValue !== false) { + valeur = evaluateAttribute(valeur) + } + return { + ...node, + nodeValue: + condition.nodeValue == null || condition.nodeValue === false + ? condition.nodeValue + : valeur.nodeValue, + explanation: { valeur, condition }, + missingVariables: mergeMissing( + valeur.missingVariables, + bonus(condition.missingVariables) + ), + unit: valeur.unit + } +} + +export default function Applicable(recurse, v) { + const explanation = { + valeur: recurse(v.valeur), + condition: recurse(v['applicable si']) + } + return { + evaluate, + jsx: MecanismApplicable, + explanation, + category: 'mecanism', + name: Applicable.name, + unit: explanation.valeur.unit + } +} + +Applicable.nom = 'applicable si' diff --git a/publicodes/source/mecanisms/arrondi.tsx b/publicodes/source/mecanisms/arrondi.tsx index 26cf7b72a..d57d7aaca 100644 --- a/publicodes/source/mecanisms/arrondi.tsx +++ b/publicodes/source/mecanisms/arrondi.tsx @@ -1,37 +1,20 @@ -import { has } from 'ramda' import React from 'react' -import { Trans } from 'react-i18next' import { InfixMecanism } from '../components/mecanisms/common' -import { defaultNode, evaluateNode, mergeAllMissing } from '../evaluation' -import { simplifyNodeUnit } from '../nodeUnits' -import { mapTemporal, pureTemporal, temporalAverage } from '../temporal' -import { EvaluatedNode, EvaluatedRule } from '../types' -import { serializeUnit } from '../units' - -type MecanismRoundProps = { - explanation: ArrondiExplanation -} +import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' +import { EvaluatedNode } from '../types' export type ArrondiExplanation = { - value: EvaluatedNode - decimals: EvaluatedNode + valeur: EvaluatedNode + arrondi: EvaluatedNode } -function MecanismRound({ explanation }: MecanismRoundProps) { +function MecanismArrondi({ explanation }) { return ( - - {explanation.decimals.nodeValue !== false && - explanation.decimals.isDefault != false && ( -

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

- )} + +

+ Arrondi : + {makeJsx(explanation.arrondi)} +

) } @@ -40,66 +23,52 @@ function roundWithPrecision(n: number, fractionDigits: number) { return +n.toFixed(fractionDigits) } -function evaluate( - cache, - situation, - parsedRules, - node: EvaluatedRule -) { +const evaluate = (cache, situation, parsedRules, node) => { const evaluateAttribute = evaluateNode.bind( null, cache, situation, parsedRules ) - const value = simplifyNodeUnit(evaluateAttribute(node.explanation.value)) - const decimals = evaluateAttribute(node.explanation.decimals) + const valeur = evaluateAttribute(node.explanation.valeur) + const nodeValue = valeur.nodeValue + let arrondi = node.explanation.arrondi + if (nodeValue !== false) { + arrondi = evaluateAttribute(arrondi) + } - const temporalValue = mapTemporal( - (val: number | false | null) => - typeof val === 'number' - ? roundWithPrecision(val, decimals.nodeValue) - : val, - value.temporalValue ?? pureTemporal(value.nodeValue) - ) - - const nodeValue = temporalAverage(temporalValue, value.unit) return { ...node, - unit: value.unit, - nodeValue, - ...(temporalValue.length > 1 && { temporalValue }), - missingVariables: mergeAllMissing([value, decimals]), - explanation: { value, decimals } + nodeValue: + typeof valeur.nodeValue !== 'number' + ? valeur.nodeValue + : typeof arrondi.nodeValue === 'number' + ? roundWithPrecision(valeur.nodeValue, arrondi.nodeValue) + : arrondi.nodeValue === true + ? roundWithPrecision(valeur.nodeValue, 0) + : arrondi.nodeValue === null + ? null + : valeur.nodeValue, + explanation: { valeur, arrondi }, + missingVariables: mergeAllMissing([valeur, arrondi]), + unit: valeur.unit } } -export default (recurse, v) => { +export default function Arrondi(recurse, v) { const explanation = { - value: has('valeur', v) ? recurse(v['valeur']) : recurse(v), - decimals: has('décimales', v) ? recurse(v['décimales']) : defaultNode(0) - } as ArrondiExplanation - + valeur: recurse(v.valeur), + arrondi: recurse(v.arrondi) + } return { - explanation, evaluate, - jsx: MecanismRound, + jsx: MecanismArrondi, + explanation, category: 'mecanism', name: 'arrondi', type: 'numeric', - unit: explanation.value.unit + unit: explanation.valeur.unit } } -export function unchainRoundMecanism(recurse, rawNode) { - const { arrondi, ...valeur } = rawNode - const arrondiValue = recurse(arrondi) - - if (serializeUnit(arrondiValue.unit) === 'décimales') { - return { arrondi: { valeur, décimales: arrondiValue.nodeValue } } - } else if (arrondiValue.nodeValue === true) { - return { arrondi: { valeur } } - } else { - return valeur - } -} +Arrondi.nom = 'arrondi' diff --git a/publicodes/source/mecanisms/durée.tsx b/publicodes/source/mecanisms/durée.tsx index 2492858a2..40074523f 100644 --- a/publicodes/source/mecanisms/durée.tsx +++ b/publicodes/source/mecanisms/durée.tsx @@ -3,7 +3,7 @@ import { defaultNode, evaluateNode, makeJsx, - mergeMissing, + mergeAllMissing, parseObject } from '../evaluation' import { Mecanism } from '../components/mecanisms/common' @@ -54,10 +54,7 @@ const evaluate = (cache, situation, parsedRules, node) => { ) ) } - const missingVariables = mergeMissing( - from.missingVariables, - to.missingVariables - ) + const missingVariables = mergeAllMissing([from, to]) return { ...node, missingVariables, diff --git a/publicodes/source/mecanisms/encadrement.tsx b/publicodes/source/mecanisms/encadrement.tsx deleted file mode 100644 index b19ab57ef..000000000 --- a/publicodes/source/mecanisms/encadrement.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react' -import { InfixMecanism } from '../components/mecanisms/common' -import { typeWarning } from '../error' -import { - defaultNode, - evaluateObject, - makeJsx, - parseObject -} from '../evaluation' -import { convertNodeToUnit } from '../nodeUnits' - -function MecanismEncadrement({ nodeValue, explanation }) { - return ( - - {!explanation.plancher.isDefault && ( - <> -

- Minimum : - {makeJsx(explanation.plancher)} -

- - )} - {!explanation.plafond.isDefault && ( - <> -

- Plafonné à : - {makeJsx(explanation.plafond)} -

- - )} -
- ) -} - -const objectShape = { - valeur: false, - plafond: defaultNode(Infinity), - plancher: defaultNode(-Infinity) -} - -const evaluate = evaluateObject( - objectShape, - ({ valeur, plafond, plancher }, cache) => { - if (plafond.nodeValue === false || plafond.nodeValue === null) { - plafond = objectShape.plafond - } - - if (valeur.unit) { - try { - plafond = convertNodeToUnit(valeur.unit, plafond) - plancher = convertNodeToUnit(valeur.unit, plancher) - } catch (e) { - typeWarning( - cache._meta.contextRule, - "Le plafond / plancher de l'encadrement a une unité incompatible avec celle de la valeur à encadrer", - e - ) - } - } - return { - nodeValue: - typeof valeur.nodeValue !== 'number' - ? valeur.nodeValue - : Math.max( - plancher.nodeValue, - Math.min(plafond.nodeValue, valeur.nodeValue) - ), - unit: valeur.unit - } - } -) - -export default (recurse, v) => { - const explanation = parseObject(recurse, objectShape, v) - - return { - evaluate, - jsx: MecanismEncadrement, - explanation, - category: 'mecanism', - name: 'encadrement', - type: 'numeric', - unit: explanation.valeur.unit - } -} diff --git a/publicodes/source/mecanisms/group.tsx b/publicodes/source/mecanisms/group.tsx deleted file mode 100644 index b228e59f2..000000000 --- a/publicodes/source/mecanisms/group.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { map } from 'ramda' -import { evaluateNode, mergeMissing } from '../evaluation' - -const evaluate = (cache, situation, parsedRules, node) => { - const explanation = map( - node => evaluateNode(cache, situation, parsedRules, node), - node.explanation - ) - const evaluation = Object.entries(explanation).reduce( - ({ missingVariables, nodeValue }, [name, evaluation]) => { - const mergedMissingVariables = mergeMissing( - missingVariables, - evaluation.missingVariables - ) - if (evaluation.explanation.isApplicable === false) { - return { missingVariables: mergedMissingVariables, nodeValue } - } - return { - missingVariables: mergedMissingVariables, - nodeValue: { - [name]: evaluation.nodeValue, - ...nodeValue - } - } - }, - { missingVariables: {}, nodeValue: {} } - ) - - return { ...evaluation, explanation, ...node } -} - -export const mecanismGroup = (rules: Array, dottedName: string) => ( - parse, - k, - v -) => { - let références: Array - if (v === 'tous') { - références = rules - .filter( - name => - name.startsWith(dottedName) && - name.split(' . ').length === dottedName.split(' . ').length + 1 - ) - .map(name => name.split(' . ').slice(-1)[0]) - } else { - références = v - } - const parsedRéférences = références.reduce( - (acc, name) => ({ - ...acc, - [name]: parse(name) - }), - {} - ) - return { - explanation: parsedRéférences, - evaluate, - jsx: function Groupe({ explanation }) { - return null - }, - category: 'mecanism', - name: 'groupe', - type: 'groupe' - } -} diff --git a/publicodes/source/mecanisms/nonApplicable.tsx b/publicodes/source/mecanisms/nonApplicable.tsx new file mode 100644 index 000000000..a5128e1c6 --- /dev/null +++ b/publicodes/source/mecanisms/nonApplicable.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import { InfixMecanism } from '../components/mecanisms/common' +import { + bonus, + evaluateNode, + makeJsx, + mergeAllMissing, + mergeMissing +} from '../evaluation' + +function MecanismNonApplicable({ explanation }) { + return ( + +

+ Non applicable si : + {makeJsx(explanation.applicable)} +

+
+ ) +} + +const evaluate = (cache, situation, parsedRules, node) => { + const evaluateAttribute = evaluateNode.bind( + null, + cache, + situation, + parsedRules + ) + const condition = evaluateAttribute(node.explanation.condition) + let valeur = node.explanation.valeur + if (condition.nodeValue !== true) { + valeur = evaluateAttribute(valeur) + } + return { + ...node, + nodeValue: + condition.nodeValue == null + ? condition.nodeValue + : condition.nodeValue === true + ? false + : valeur.nodeValue, + explanation: { valeur, condition }, + missingVariables: mergeMissing( + valeur.missingVariables, + bonus(condition.missingVariables) + ), + unit: valeur.unit + } +} + +export default function NonApplicable(recurse, v) { + const explanation = { + valeur: recurse(v.valeur), + condition: recurse(v['non applicable si']) + } + return { + evaluate, + jsx: MecanismNonApplicable, + explanation, + category: 'mecanism', + name: 'non applicable', + unit: explanation.valeur.unit + } +} + +NonApplicable.nom = 'non applicable si' diff --git a/publicodes/source/mecanisms/operation.tsx b/publicodes/source/mecanisms/operation.tsx index 64aa4eced..c7b7a453a 100644 --- a/publicodes/source/mecanisms/operation.tsx +++ b/publicodes/source/mecanisms/operation.tsx @@ -3,7 +3,7 @@ import React from 'react' import { Operation } from '../components/mecanisms/common' import { convertToDate } from '../date' import { typeWarning } from '../error' -import { evaluateNode, makeJsx, mergeMissing } from '../evaluation' +import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' import { convertNodeToUnit } from '../nodeUnits' import { liftTemporal2, pureTemporal, temporalAverage } from '../temporal' import { inferUnit, serializeUnit } from '../units' @@ -15,10 +15,7 @@ export default (k, operatorFunction, symbol) => (recurse, v) => { node.explanation ) let [node1, node2] = explanation - const missingVariables = mergeMissing( - node1.missingVariables, - node2.missingVariables - ) + const missingVariables = mergeAllMissing([node1, node2]) if (node1.nodeValue == null || node2.nodeValue == null) { return { ...node, nodeValue: null, explanation, missingVariables } diff --git a/publicodes/source/mecanisms/plafond.tsx b/publicodes/source/mecanisms/plafond.tsx new file mode 100644 index 000000000..46dda8c5c --- /dev/null +++ b/publicodes/source/mecanisms/plafond.tsx @@ -0,0 +1,82 @@ +import React from 'react' +import { InfixMecanism } from '../components/mecanisms/common' +import { typeWarning } from '../error' +import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' +import { convertNodeToUnit } from '../nodeUnits' + +function MecanismPlafond({ explanation }) { + return ( + +

+ Plafonné à : + {makeJsx(explanation.plafond)} +

+
+ ) +} + +const evaluate = (cache, situation, parsedRules, node) => { + const evaluateAttribute = evaluateNode.bind( + null, + cache, + situation, + parsedRules + ) + const valeur = evaluateAttribute(node.explanation.valeur) + + let nodeValue = valeur.nodeValue + let plafond = node.explanation.plafond + if (nodeValue !== false) { + plafond = evaluateAttribute(plafond) + if (valeur.unit) { + try { + plafond = convertNodeToUnit(valeur.unit, plafond) + } catch (e) { + typeWarning( + cache._meta.contextRule, + "L'unité du plafond n'est pas compatible avec celle de la valeur à encadrer", + e + ) + } + } + } + if ( + typeof nodeValue === 'number' && + typeof plafond.nodeValue === 'number' && + nodeValue > plafond.nodeValue + ) { + nodeValue = plafond.nodeValue + plafond.isActive = true + } + return { + ...node, + nodeValue, + unit: valeur.unit, + explanation: { valeur, plafond }, + missingVariables: mergeAllMissing([valeur, plafond]) + } +} + +export default function Plafond(recurse, v) { + const explanation = { + valeur: recurse(v.valeur), + plafond: recurse(v.plafond) + } + return { + evaluate, + jsx: MecanismPlafond, + explanation, + category: 'mecanism', + name: 'plafond', + type: 'numeric', + unit: explanation.valeur.unit + } +} + +Plafond.nom = 'plafond' diff --git a/publicodes/source/mecanisms/plancher.tsx b/publicodes/source/mecanisms/plancher.tsx new file mode 100644 index 000000000..ec144cf7a --- /dev/null +++ b/publicodes/source/mecanisms/plancher.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import { InfixMecanism } from '../components/mecanisms/common' +import { typeWarning } from '../error' +import { evaluateNode, makeJsx, mergeAllMissing } from '../evaluation' +import { convertNodeToUnit } from '../nodeUnits' + +function MecanismPlancher({ explanation }) { + return ( + +

+ Minimum : + {makeJsx(explanation.plancher)} +

+
+ ) +} + +const evaluate = (cache, situation, parsedRules, node) => { + const evaluateAttribute = evaluateNode.bind( + null, + cache, + situation, + parsedRules + ) + const valeur = evaluateAttribute(node.explanation.valeur) + let nodeValue = valeur.nodeValue + let plancher = node.explanation.plancher + if (nodeValue !== false) { + plancher = evaluateAttribute(plancher) + if (valeur.unit) { + try { + plancher = convertNodeToUnit(valeur.unit, plancher) + } catch (e) { + typeWarning( + cache._meta.contextRule, + "L'unité du plancher n'est pas compatible avec celle de la valeur à encadrer", + e + ) + } + } + } + if ( + typeof nodeValue === 'number' && + typeof plancher.nodeValue === 'number' && + nodeValue < plancher.nodeValue + ) { + nodeValue = plancher.nodeValue + plancher.isActive = true + } + return { + ...node, + nodeValue, + explanation: { valeur, plancher }, + missingVariables: mergeAllMissing([valeur, plancher]), + unit: valeur.unit + } +} + +export default function Plancher(recurse, v) { + const explanation = { + valeur: recurse(v.valeur), + plancher: recurse(v.plancher) + } + return { + evaluate, + jsx: MecanismPlancher, + explanation, + category: 'mecanism', + name: 'plancher', + type: 'numeric', + unit: explanation.valeur.unit + } +} + +Plancher.nom = 'plancher' diff --git a/publicodes/source/mecanisms/product.tsx b/publicodes/source/mecanisms/product.tsx index 2ef5d01d1..de4383ec9 100644 --- a/publicodes/source/mecanisms/product.tsx +++ b/publicodes/source/mecanisms/product.tsx @@ -1,7 +1,7 @@ import Product from '../components/mecanisms/Product' import { typeWarning } from '../error' import { defaultNode, evaluateObject, parseObject } from '../evaluation' -import { convertNodeToUnit } from '../nodeUnits' +import { convertNodeToUnit, simplifyNodeUnit } from '../nodeUnits' import { areUnitConvertible, convertUnit, inferUnit } from '../units' export const mecanismProduct = (recurse, v) => { @@ -45,13 +45,13 @@ export const mecanismProduct = (recurse, v) => { nodeValue = convertUnit(unit, assiette.unit, nodeValue) unit = assiette.unit } - return { + return simplifyNodeUnit({ nodeValue, unit, explanation: { plafondActif: assiette.nodeValue > plafond.nodeValue } - } + }) } const explanation = parseObject(recurse, objectShape, v), evaluate = evaluateObject(objectShape, effect) diff --git a/publicodes/source/parse.tsx b/publicodes/source/parse.tsx index f2e7ba2da..cdc815323 100644 --- a/publicodes/source/parse.tsx +++ b/publicodes/source/parse.tsx @@ -13,18 +13,22 @@ import { lt, lte, multiply, + omit, subtract } from 'ramda' import React from 'react' import { EngineError, syntaxError } from './error' import { formatValue } from './format' import grammar from './grammar.ne' -import mecanismRound, { unchainRoundMecanism } from './mecanisms/arrondi' +import arrondi from './mecanisms/arrondi' import barème from './mecanisms/barème' import { mecanismAllOf } from './mecanisms/condition-allof' import { mecanismOneOf } from './mecanisms/condition-oneof' import durée from './mecanisms/durée' -import encadrement from './mecanisms/encadrement' +import plafond from './mecanisms/plafond' +import plancher from './mecanisms/plancher' +import applicable from './mecanisms/applicable' +import nonApplicable from './mecanisms/nonApplicable' import grille from './mecanisms/grille' import { mecanismInversion } from './mecanisms/inversion' import { mecanismMax } from './mecanisms/max' @@ -98,22 +102,19 @@ Les mécanisme possibles sont : 'somme', 'le maximum de', 'le minimum de', 'tout ` ) } - const keys = Object.keys(rawNode) - const unchainableMecanisms = difference(keys, chainableMecanisms) - if (keys.length > 1) { - if (unchainableMecanisms.length > 1) { - syntaxError( - rule.dottedName, - ` -Les mécanismes suivants se situent au même niveau : ${unchainableMecanisms - .map(x => `'${x}'`) - .join(', ')} -Cela vient probablement d'une erreur dans l'indentation - ` - ) - } - return parseChainedMecanisms(rules, rule, parsedRules, rawNode) + rawNode = unfoldChainedMecanisms(rawNode) + const keys = Object.keys(rawNode) + if (keys.length > 1) { + syntaxError( + rule.dottedName, + ` +Les mécanismes suivants se situent au même niveau : ${keys + .map(x => `'${x}'`) + .join(', ')} +Cela vient probablement d'une erreur dans l'indentation + ` + ) } const mecanismName = Object.keys(rawNode)[0] const values = rawNode[mecanismName] @@ -173,32 +174,34 @@ Vérifiez qu'il n'y ait pas d'erreur dans l'orthographe du nom.` } } -const chainableMecanisms = ['arrondi', 'plancher', 'plafond'] - -function parseChainedMecanisms(rules, rule, parsedRules, rawNode) { - const keys = Object.keys(rawNode) - const recurse = parseMecanism(rules, rule, parsedRules) - if (keys.includes('arrondi')) { - return recurse( - unchainRoundMecanism(parse(rules, rule, parsedRules), rawNode) - ) - } else if (keys.includes('plancher')) { - const { plancher, ...valeur } = rawNode - return recurse({ - encadrement: { - valeur, - plancher - } - }) - } else if (keys.includes('plafond')) { - const { plafond, ...valeur } = rawNode - return recurse({ - encadrement: { - valeur, - plafond - } - }) +const chainableMecanisms = [ + applicable, + nonApplicable, + plancher, + plafond, + arrondi +] +function unfoldChainedMecanisms(rawNode) { + if (Object.keys(rawNode).length === 1) { + return rawNode } + return chainableMecanisms.reduceRight( + (node, parseFn) => { + if (!(parseFn.nom in rawNode)) { + return node + } + return { + [parseFn.nom]: { + [parseFn.nom]: rawNode[parseFn.nom], + valeur: node + } + } + }, + omit( + chainableMecanisms.map(fn => fn.nom), + rawNode + ) + ) } const knownOperations = { @@ -223,6 +226,7 @@ const operationDispatch = fromPairs( const statelessParseFunction = { ...operationDispatch, + ...chainableMecanisms.reduce((acc, fn) => ({ [fn.nom]: fn, ...acc }), {}), 'une de ces conditions': mecanismOneOf, 'toutes ces conditions': mecanismAllOf, somme: mecanismSum, @@ -230,11 +234,9 @@ const statelessParseFunction = { multiplication: mecanismProduct, produit: mecanismProduct, temporalValue: variableTemporelle, - arrondi: mecanismRound, barème, grille, 'taux progressif': tauxProgressif, - encadrement, durée, 'le maximum de': mecanismMax, 'le minimum de': mecanismMin, diff --git a/publicodes/source/parseRules.ts b/publicodes/source/parseRules.ts index 166811f02..fc0ef59ca 100644 --- a/publicodes/source/parseRules.ts +++ b/publicodes/source/parseRules.ts @@ -1,6 +1,6 @@ import parseRule from './parseRule' import yaml from 'yaml' -import { lensPath, set } from 'ramda' +import { compose, dissoc, lensPath, over, set } from 'ramda' import { compilationError } from './error' import { parseReference } from './parseReference' import { ParsedRules, Rules } from './types' @@ -63,7 +63,6 @@ export default function parseRules( ...other })) }) - return parsedRules as ParsedRules } @@ -82,6 +81,7 @@ function extractInlinedNames(rules: Record>) { context: Array = [] ) => ([key, value]: [string, Record]) => { const match = /\[ref( (.+))?\]$/.exec(key) + if (match) { const argumentType = key.replace(match[0], '').trim() const argumentName = match[2]?.trim() || argumentType @@ -93,18 +93,17 @@ function extractInlinedNames(rules: Record>) { `Le paramètre [ref] ${argumentName} entre en conflit avec la règle déjà existante ${extractedReferenceName}` ) } - rules[extractedReferenceName] = { formule: value, // The `virtualRule` parameter is used to avoid creating a // dedicated documentation page. virtualRule: true } - rules[dottedName] = set( - lensPath([...context, argumentType]), - extractedReferenceName, - rules[dottedName] - ) + + rules[dottedName] = compose( + over(lensPath(context), dissoc(key)) as any, + set(lensPath([...context, argumentType]), extractedReferenceName) + )(rules[dottedName]) as any extractNamesInRule(extractedReferenceName) } else if (Array.isArray(value)) { value.forEach((content: Record, i) => diff --git a/publicodes/test/mecanisms.test.js b/publicodes/test/mecanisms.test.js index 154b5c4f3..20c270c4c 100644 --- a/publicodes/test/mecanisms.test.js +++ b/publicodes/test/mecanisms.test.js @@ -12,6 +12,7 @@ import { coerceArray } from '../source/utils' import testSuites from './load-mecanism-tests' testSuites.forEach(([suiteName, suite]) => { const engine = new Engine(suite) + describe(`Mécanisme ${suiteName}`, () => { Object.entries(suite) .filter(([, rule]) => rule?.exemples) diff --git a/publicodes/test/mécanismes/applicable.yaml b/publicodes/test/mécanismes/applicable.yaml index d85620ed8..7e63c942c 100644 --- a/publicodes/test/mécanismes/applicable.yaml +++ b/publicodes/test/mécanismes/applicable.yaml @@ -15,3 +15,16 @@ prévoyance obligatoire cadre: situation: statut cadre: non valeur attendue: false + + +variable: + par défaut: oui +applicable comme mécanisme chainé: + formule: + applicable si: variable + valeur: 5 + exemples: + - valeur attendue: 5 + - situation: + variable: non + valeur attendue: false diff --git a/publicodes/test/mécanismes/arrondi.yaml b/publicodes/test/mécanismes/arrondi.yaml index 29fcf10f6..1b03c7af8 100644 --- a/publicodes/test/mécanismes/arrondi.yaml +++ b/publicodes/test/mécanismes/arrondi.yaml @@ -2,13 +2,15 @@ cotisation retraite: demie part: formule: - arrondi: 50% * 100.2€ + valeur: 0.5 * 100.2€ + arrondi: oui exemples: - valeur attendue: 50 Arrondi: formule: - arrondi: cotisation retraite + valeur: cotisation retraite + arrondi: oui exemples: - nom: arrondi en dessous @@ -24,9 +26,8 @@ nombre de décimales: Arrondi avec precision: formule: - arrondi: - valeur: cotisation retraite - décimales: nombre de décimales + valeur: cotisation retraite + arrondi: nombre de décimales exemples: - nom: pas de décimales situation: diff --git a/publicodes/test/mécanismes/encadrement.yaml b/publicodes/test/mécanismes/encadrement.yaml index 034ba1ae8..cd60d1382 100644 --- a/publicodes/test/mécanismes/encadrement.yaml +++ b/publicodes/test/mécanismes/encadrement.yaml @@ -1,8 +1,7 @@ plafonnement: formule: - encadrement: - valeur: 1000 € - plafond: 250 € + valeur: 1000 € + plafond: 250 € exemples: - valeur attendue: 250 @@ -25,18 +24,16 @@ plancher nouvelle ecriture: plafonnement inactif: formule: - encadrement: - valeur: 1000 € - plafond: non + valeur: 1000 € + plafond: non exemples: - valeur attendue: 1000 plafonnement reference inactive: formule: - encadrement: - valeur: 1000 € - plafond: plafond + valeur: 1000 € + plafond: plafond exemples: - valeur attendue: 1000 @@ -44,9 +41,19 @@ plafonnement reference inactive: plafonnement reference inactive . plafond: non plancher: formule: - encadrement: - valeur: 1000 € - plancher: 2500 € + valeur: 1000 € + plancher: 2500 € exemples: - valeur attendue: 2500 + + +encadrement inférieur et supérieur: + formule: + somme: + - 500 + - 400 + plafond: 800 + plancher: 200 + exemples: + - valeur attendue: 800 diff --git a/publicodes/test/mécanismes/paramètres-nommés.yaml b/publicodes/test/mécanismes/paramètres-nommés.yaml index 1c6696b45..53e019bb2 100644 --- a/publicodes/test/mécanismes/paramètres-nommés.yaml +++ b/publicodes/test/mécanismes/paramètres-nommés.yaml @@ -19,13 +19,11 @@ paramètre nommés imbriqués: formule: multiplication: assiette [ref]: - encadrement: - valeur: 1000€ - plafond [ref]: 100€ + valeur: 1000€ + plafond [ref]: 100€ taux: 5% exemples: - valeur attendue: 5 - - situation: paramètre nommés imbriqués . assiette . plafond: 200 valeur attendue: 10 diff --git a/publicodes/test/mécanismes/recalcul.yaml b/publicodes/test/mécanismes/recalcul.yaml index c1e36b37d..04dd421d5 100644 --- a/publicodes/test/mécanismes/recalcul.yaml +++ b/publicodes/test/mécanismes/recalcul.yaml @@ -15,11 +15,10 @@ SMIC net: Recalcule règle courante: formule: - encadrement: - valeur: 10% * salaire brut - plafond: - recalcul: - avec: - salaire brut: 100€ + valeur: 10% * salaire brut + plafond: + recalcul: + avec: + salaire brut: 100€ exemples: - valeur attendue: 10