Merge pull request #1167 from betagouv/refacto-applicable

transforme applicable si et non applicable si en mécanisme chainé
pull/1192/head
Johan Girod 2020-11-04 12:14:20 +01:00 committed by GitHub
commit 33ff99bc30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 558 additions and 365 deletions

View File

@ -7,7 +7,6 @@ export default function LinkToForm() {
document.referrer ||
document.location.origin
).hostname.replace(/^www\.|^m\./, '')
console.log(hostname)
return (
<div
style={{ display: 'flex', justifyContent: 'center', marginTop: '1rem' }}

View File

@ -36,7 +36,6 @@ export default function Conversation({ customEndMessages }: ConversationProps) {
dispatch(goToQuestion(currentQuestion))
}
}, [dispatch, currentQuestion])
const setDefault = () =>
dispatch(
validateStepWithValue(

View File

@ -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

View File

@ -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

View File

@ -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]"`;

View File

@ -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 (
<Mecanism name="recalcul" value={nodeValue} unit={unit}>
<>

View File

@ -102,10 +102,12 @@ export function Mecanism({
export const InfixMecanism = ({
value,
prefixed,
children
}: {
value: EvaluatedNode
children: React.ReactNode
prefixed?: boolean
}) => {
return (
<div
@ -120,8 +122,9 @@ export const InfixMecanism = ({
}
`}
>
{prefixed && children}
<div className="value">{makeJsx(value)}</div>
{children}
{!prefixed && children}
</div>
)
}

View File

@ -167,26 +167,78 @@ export function isRecalculMech<Names extends string>(
)
}
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<ASTNode>
@ -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<Names extends string> =
| RecalculMech<Names>
| EncadrementMech
| PlancherMech
| PlafondMech
| ApplicableMech
| NonApplicableMech
| SommeMech
| ProduitMech
| VariationsMech
@ -506,7 +561,10 @@ export function isAnyMechanism<Names extends string>(
): node is AnyMechanism<Names> {
return (
isRecalculMech<Names>(node) ||
isEncadrementMech(node) ||
isPlafondMech(node) ||
isPlancherMech(node) ||
isApplicableMech(node) ||
isNonApplicableMech(node) ||
isSommeMech(node) ||
isProduitMech(node) ||
isVariationsMech(node) ||

View File

@ -56,12 +56,11 @@ export function ruleDepsOfNode<Names extends string>(
return ruleReference === ruleName ? [] : [ruleReference]
}
function ruleDepsOfEncadrementMech(
encadrementMech: ASTTypes.EncadrementMech
function ruleDepsOfPlafondMech(
encadrementMech: ASTTypes.PlafondMech
): RuleDependencies<Names> {
const result = [
encadrementMech.explanation.plafond,
encadrementMech.explanation.plancher,
encadrementMech.explanation.valeur
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
@ -72,6 +71,33 @@ export function ruleDepsOfNode<Names extends string>(
return result
}
function ruleDepsOfPlancherMech(
mech: ASTTypes.PlancherMech
): RuleDependencies<Names> {
const result = [mech.explanation.plancher, mech.explanation.valeur].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfApplicableMech(
mech: ASTTypes.ApplicableMech | ASTTypes.NonApplicableMech
): RuleDependencies<Names> {
const result = [
mech.explanation.condition,
mech.explanation.valeur
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfSommeMech(
sommeMech: ASTTypes.SommeMech
): RuleDependencies<Names> {
@ -168,8 +194,8 @@ export function ruleDepsOfNode<Names extends string>(
arrondiMech: ASTTypes.ArrondiMech
): RuleDependencies<Names> {
const result = [
arrondiMech.explanation.decimals,
arrondiMech.explanation.value
arrondiMech.explanation.arrondi,
arrondiMech.explanation.valeur
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
@ -312,8 +338,14 @@ export function ruleDepsOfNode<Names extends string>(
result = ruleDepsOfPossibilities2(node)
} else if (ASTTypes.isRecalculMech<Names>(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)) {

View File

@ -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 || {})

View File

@ -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 (
<InfixMecanism prefixed value={explanation.valeur}>
<p>
<strong>Applicable si : </strong>
{makeJsx(explanation.applicable)}
</p>
</InfixMecanism>
)
}
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'

View File

@ -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<string, number>
decimals: EvaluatedNode<string, number>
valeur: EvaluatedNode<string, number>
arrondi: EvaluatedNode<string, number>
}
function MecanismRound({ explanation }: MecanismRoundProps) {
function MecanismArrondi({ explanation }) {
return (
<InfixMecanism value={explanation.value}>
{explanation.decimals.nodeValue !== false &&
explanation.decimals.isDefault != false && (
<p>
<Trans
i18nKey="arrondi-to-decimals"
count={explanation.decimals.nodeValue ?? undefined}
>
<strong>Arrondi à : </strong>
{{ count: explanation.decimals.nodeValue }} décimales
</Trans>
</p>
)}
<InfixMecanism value={explanation.valeur}>
<p>
<strong>Arrondi : </strong>
{makeJsx(explanation.arrondi)}
</p>
</InfixMecanism>
)
}
@ -40,66 +23,52 @@ function roundWithPrecision(n: number, fractionDigits: number) {
return +n.toFixed(fractionDigits)
}
function evaluate<Names extends string>(
cache,
situation,
parsedRules,
node: EvaluatedRule<Names, ArrondiExplanation>
) {
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'

View File

@ -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,

View File

@ -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 (
<InfixMecanism value={explanation.valeur}>
{!explanation.plancher.isDefault && (
<>
<p
style={
nodeValue && nodeValue === explanation.plancher.nodeValue
? { background: 'var(--lighterColor)', fontWeight: 'bold' }
: {}
}
>
<strong>Minimum : </strong>
{makeJsx(explanation.plancher)}
</p>
</>
)}
{!explanation.plafond.isDefault && (
<>
<p
style={
nodeValue && nodeValue === explanation.plancher.nodeValue
? { background: 'var(--lighterColor)', fontWeight: 'bold' }
: {}
}
>
<strong>Plafonné à : </strong>
{makeJsx(explanation.plafond)}
</p>
</>
)}
</InfixMecanism>
)
}
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
}
}

View File

@ -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<string>, dottedName: string) => (
parse,
k,
v
) => {
let références: Array<string>
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'
}
}

View File

@ -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 (
<InfixMecanism prefixed value={explanation.valeur}>
<p>
<strong>Non applicable si : </strong>
{makeJsx(explanation.applicable)}
</p>
</InfixMecanism>
)
}
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'

View File

@ -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 }

View File

@ -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 (
<InfixMecanism value={explanation.valeur}>
<p
style={
explanation.plafond.isActive
? { background: 'var(--lighterColor)', fontWeight: 'bold' }
: {}
}
>
<strong>Plafonné à : </strong>
{makeJsx(explanation.plafond)}
</p>
</InfixMecanism>
)
}
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'

View File

@ -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 (
<InfixMecanism value={explanation.valeur}>
<p
style={
explanation.plancher.isActive
? { background: 'var(--lighterColor)', fontWeight: 'bold' }
: {}
}
>
<strong>Minimum : </strong>
{makeJsx(explanation.plancher)}
</p>
</InfixMecanism>
)
}
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'

View File

@ -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)

View File

@ -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,

View File

@ -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<Names extends string>(
...other
}))
})
return parsedRules as ParsedRules<Names>
}
@ -82,6 +81,7 @@ function extractInlinedNames(rules: Record<string, Record<string, any>>) {
context: Array<string | number> = []
) => ([key, value]: [string, Record<string, any>]) => {
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<string, Record<string, any>>) {
`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<string, any>, i) =>

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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