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