- {condition && (
+ {!condition.isDefault && (
- {condition ? (
+ {!condition.isDefault ? (
Alors
) : (
Sinon
diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js
index 1e915cdcd..1ed7e258b 100644
--- a/source/engine/mecanisms.js
+++ b/source/engine/mecanisms.js
@@ -3,7 +3,6 @@ import variations from 'Engine/mecanisms/variations'
import { convertNodeToUnit } from 'Engine/nodeUnits'
import { inferUnit, isPercentUnit } from 'Engine/units'
import {
- add,
any,
equals,
evolve,
@@ -15,11 +14,9 @@ import {
path,
pluck,
reduce,
- subtract,
toPairs
} from 'ramda'
import React from 'react'
-import { Trans } from 'react-i18next'
import 'react-virtualized/styles.css'
import { typeWarning } from './error'
import {
@@ -281,7 +278,7 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => {
let evaluate = (currentCache, situationGate, parsedRules, node) => {
let defaultRuleToEvaluate = dottedNameContext
let nodeToEvaluate = recurse(node?.règle ?? defaultRuleToEvaluate)
- let cache = { _meta: currentCache._meta, _metaInRecalcul: true } // Create an empty cache
+ let cache = { _meta: { ...currentCache._meta, inRecalcul: true } } // Create an empty cache
let amendedSituation = Object.fromEntries(
Object.keys(node.avec).map(dottedName => [
disambiguateRuleReference(
@@ -293,7 +290,7 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => {
])
)
- if (currentCache._metaInRecalcul) {
+ if (currentCache._meta.inRecalcul) {
return defaultNode(false)
}
@@ -344,7 +341,10 @@ export let mecanismRecalcul = dottedNameContext => (recurse, k, v) => {
export let mecanismSum = (recurse, k, v) => {
let explanation = v.map(recurse)
- let evaluate = evaluateArray(add, 0)
+ let evaluate = evaluateArray(
+ (x, y) => (x === false && y === false ? false : x + y),
+ false
+ )
return {
evaluate,
@@ -376,7 +376,7 @@ export let mecanismReduction = (recurse, k, v) => {
cache
) => {
let v_assiette = assiette.nodeValue
- if (v_assiette == null) return null
+ if (v_assiette == null) return { nodeValue: null }
if (assiette.unit) {
try {
franchise = convertNodeToUnit(assiette.unit, franchise)
@@ -431,8 +431,8 @@ export let mecanismReduction = (recurse, k, v) => {
: montantFranchiséDécoté
return {
nodeValue,
- additionalExplanation: {
- unit: assiette.unit,
+ unit: assiette.unit,
+ explanation: {
franchise,
plafond,
abattement
@@ -509,9 +509,10 @@ export let mecanismProduct = (recurse, k, v) => {
)
return {
nodeValue,
- additionalExplanation: {
- plafondActif: assiette.nodeValue > plafond.nodeValue,
- unit
+
+ unit,
+ explanation: {
+ plafondActif: assiette.nodeValue > plafond.nodeValue
}
}
}
@@ -601,53 +602,6 @@ export let mecanismMin = (recurse, k, v) => {
}
}
-export let mecanismComplement = (recurse, k, v) => {
- if (v.composantes) {
- //mécanisme de composantes. Voir known-mecanisms.md/composantes
- return decompose(recurse, k, v)
- }
-
- let objectShape = { cible: false, montant: false }
- let effect = ({ cible, montant }) => {
- let nulled = cible.nodeValue == null
- return nulled
- ? null
- : subtract(montant.nodeValue, min(cible.nodeValue, montant.nodeValue))
- }
- let explanation = parseObject(recurse, objectShape, v)
-
- return {
- evaluate: evaluateObject(objectShape, effect),
- explanation,
- type: 'numeric',
- category: 'mecanism',
- name: 'complément pour atteindre',
- // eslint-disable-next-line
- jsx: (nodeValue, explanation) => (
-
-
- -
-
- cible:{' '}
-
- {makeJsx(explanation.cible)}
-
- -
-
- montant à atteindre:{' '}
-
- {makeJsx(explanation.montant)}
-
-
-
- )
- }
-}
-
export let mecanismSynchronisation = (recurse, k, v) => {
let evaluate = (cache, situationGate, parsedRules, node) => {
let APIExplanation = evaluateNode(
diff --git a/source/engine/mecanisms/barème.ts b/source/engine/mecanisms/barème.ts
index 2ebd8aa4e..663f3b269 100644
--- a/source/engine/mecanisms/barème.ts
+++ b/source/engine/mecanisms/barème.ts
@@ -1,8 +1,16 @@
+import { evaluationError } from 'Engine/error'
import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation'
import { decompose } from 'Engine/mecanisms/utils'
import variations from 'Engine/mecanisms/variations'
import Barème from 'Engine/mecanismViews/Barème'
-import { convertUnit, parseUnit } from '../units'
+import {
+ liftTemporal2,
+ liftTemporalNode,
+ mapTemporal,
+ temporalAverage
+} from 'Engine/temporal'
+import { convertUnit } from '../units'
+import { parseUnit } from './../units'
import {
evaluatePlafondUntilActiveTranche,
parseTranches
@@ -34,29 +42,27 @@ export default function parse(parse, k, v) {
}
}
-const evaluate = (
- cache,
- situationGate,
- parsedRules,
- node: ReturnType
-) => {
- const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
- const assiette = evaluate(node.explanation.assiette)
- const multiplicateur = evaluate(node.explanation.multiplicateur)
- const tranches = evaluatePlafondUntilActiveTranche(
- evaluate,
- {
- parsedTranches: node.explanation.tranches,
- assiette,
- multiplicateur
- },
- cache
- ).map(tranche => {
+function evaluateBarème(tranches, assiette, evaluate, cache) {
+ return tranches.map(tranche => {
if (tranche.isAfterActive) {
return { ...tranche, nodeValue: 0 }
}
const taux = evaluate(tranche.taux)
- if ([taux.nodeValue, tranche.nodeValue].some(value => value === null)) {
+ if (taux.temporalValue) {
+ evaluationError(
+ cache._meta.contextRule,
+ "Le taux d'une tranche ne peut pas être une valeur temporelle"
+ )
+ }
+
+ if (
+ [
+ assiette.nodeValue,
+ taux.nodeValue,
+ tranche.plafondValue,
+ tranche.plancherValue
+ ].some(value => value === null)
+ ) {
return {
...tranche,
taux,
@@ -71,20 +77,61 @@ const evaluate = (
nodeValue:
(Math.min(assiette.nodeValue, tranche.plafondValue) -
tranche.plancherValue) *
- convertUnit(taux.unit, parseUnit(''), taux.nodeValue)
+ convertUnit(taux.unit, parseUnit(''), taux.nodeValue as number)
}
})
+}
+const evaluate = (
+ cache,
+ situationGate,
+ parsedRules,
+ node: ReturnType
+) => {
+ const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
+ const assiette = evaluate(node.explanation.assiette)
+ const multiplicateur = evaluate(node.explanation.multiplicateur)
+ const temporalTranchesPlafond = liftTemporal2(
+ (assiette, multiplicateur) =>
+ evaluatePlafondUntilActiveTranche(
+ evaluate,
+ {
+ parsedTranches: node.explanation.tranches,
+ assiette,
+ multiplicateur
+ },
+ cache
+ ),
+ liftTemporalNode(assiette),
+ liftTemporalNode(multiplicateur)
+ )
+ const temporalTranches = liftTemporal2(
+ (tranches, assiette) => evaluateBarème(tranches, assiette, evaluate, cache),
+ temporalTranchesPlafond,
+ liftTemporalNode(assiette)
+ )
+ const temporalValue = mapTemporal(
+ tranches =>
+ tranches.reduce(
+ (value, { nodeValue }) =>
+ nodeValue == null ? null : value + nodeValue,
+ 0
+ ),
+ temporalTranches
+ )
return {
...node,
- nodeValue: tranches.reduce(
- (value, { nodeValue }) => (nodeValue == null ? null : value + nodeValue),
- 0
- ),
- missingVariables: mergeAllMissing(tranches),
+ nodeValue: temporalAverage(temporalValue),
+ ...(temporalValue.length > 1
+ ? {
+ temporalValue
+ }
+ : { missingVariables: mergeAllMissing(temporalTranches[0].value) }),
explanation: {
assiette,
multiplicateur,
- tranches
+ ...(temporalTranches.length > 1
+ ? { temporalTranches }
+ : { tranches: temporalTranches[0].value })
},
unit: assiette.unit
}
diff --git a/source/engine/mecanisms/durée.tsx b/source/engine/mecanisms/durée.tsx
index 7d9c1438f..d7de95d5f 100644
--- a/source/engine/mecanisms/durée.tsx
+++ b/source/engine/mecanisms/durée.tsx
@@ -1,4 +1,4 @@
-import { convertToDate, normalizeDate } from 'Engine/date'
+import { convertToDate, convertToString } from 'Engine/date'
import {
defaultNode,
evaluateNode,
@@ -26,12 +26,7 @@ function MecanismDurée({ nodeValue, explanation, unit }) {
)
}
-const today = new Date()
-const todayString = normalizeDate(
- today.getFullYear(),
- today.getMonth() + 1,
- today.getDate()
-)
+const todayString = convertToString(new Date())
const objectShape = {
depuis: defaultNode(todayString),
diff --git a/source/engine/mecanisms/grille.ts b/source/engine/mecanisms/grille.ts
index 1fd819fdd..3c1fbfd4e 100644
--- a/source/engine/mecanisms/grille.ts
+++ b/source/engine/mecanisms/grille.ts
@@ -2,6 +2,12 @@ import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation'
import { decompose } from 'Engine/mecanisms/utils'
import variations from 'Engine/mecanisms/variations'
import grille from 'Engine/mecanismViews/Grille'
+import {
+ liftTemporal2,
+ liftTemporalNode,
+ mapTemporal,
+ temporalAverage
+} from 'Engine/temporal'
import { parseUnit } from 'Engine/units'
import { lensPath, over } from 'ramda'
import {
@@ -35,25 +41,8 @@ export default function parse(parse, k, v) {
unit: explanation.tranches[0].montant.unit
}
}
-
-const evaluate = (
- cache,
- situationGate,
- parsedRules,
- node: ReturnType
-) => {
- const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
- const assiette = evaluate(node.explanation.assiette)
- const multiplicateur = evaluate(node.explanation.multiplicateur)
- const tranches = evaluatePlafondUntilActiveTranche(
- evaluate,
- {
- parsedTranches: node.explanation.tranches,
- assiette,
- multiplicateur
- },
- cache
- ).map(tranche => {
+const evaluateGrille = (tranches, evaluate) =>
+ tranches.map(tranche => {
if (tranche.isActive === false) {
return tranche
}
@@ -67,19 +56,65 @@ const evaluate = (
}
})
- const activeTranches = tranches.filter(({ isActive }) => isActive != false)
- const missingVariables = mergeAllMissing(activeTranches)
- const nodeValue = activeTranches.length ? activeTranches[0].nodeValue : false
+const evaluate = (
+ cache,
+ situationGate,
+ parsedRules,
+ node: ReturnType
+) => {
+ const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
+ const assiette = evaluate(node.explanation.assiette)
+ const multiplicateur = evaluate(node.explanation.multiplicateur)
+ const temporalTranchesPlafond = liftTemporal2(
+ (assiette, multiplicateur) =>
+ evaluatePlafondUntilActiveTranche(
+ evaluate,
+ {
+ parsedTranches: node.explanation.tranches,
+ assiette,
+ multiplicateur
+ },
+ cache
+ ),
+ liftTemporalNode(assiette),
+ liftTemporalNode(multiplicateur)
+ )
+ const temporalTranches = mapTemporal(
+ tranches => evaluateGrille(tranches, evaluate),
+ temporalTranchesPlafond
+ )
+
+ const activeTranches = mapTemporal(tranches => {
+ const activeTranche = tranches.find(tranche => tranche.isActive)
+ if (activeTranche) {
+ return [activeTranche]
+ }
+ const lastTranche = tranches[tranches.length - 1]
+ if (lastTranche.isAfterActive === false) {
+ return [{ nodeValue: false }]
+ }
+ return tranches.filter(tranche => tranche.isActive === null)
+ }, temporalTranches)
+ const temporalValue = mapTemporal(
+ tranches => (tranches[0].isActive === null ? null : tranches[0].nodeValue),
+ activeTranches
+ )
return {
...node,
+ nodeValue: temporalAverage(temporalValue),
+ ...(temporalValue.length > 1
+ ? {
+ temporalValue
+ }
+ : { missingVariables: mergeAllMissing(activeTranches[0].value) }),
explanation: {
- tranches,
assiette,
- multiplicateur
+ multiplicateur,
+ ...(temporalTranches.length > 1
+ ? { temporalTranches }
+ : { tranches: temporalTranches[0].value })
},
- missingVariables,
- nodeValue,
- unit: activeTranches[0]?.unit ?? node.unit
+ unit: activeTranches[0].value[0]?.unit ?? node.unit
}
}
diff --git a/source/engine/mecanisms/operation.js b/source/engine/mecanisms/operation.js
index 8587e2806..8137e631b 100644
--- a/source/engine/mecanisms/operation.js
+++ b/source/engine/mecanisms/operation.js
@@ -1,12 +1,14 @@
+import { convertToDate } from 'Engine/date'
import { typeWarning } from 'Engine/error'
import { evaluateNode, makeJsx, mergeMissing } from 'Engine/evaluation'
import { Node } from 'Engine/mecanismViews/common'
import { convertNodeToUnit } from 'Engine/nodeUnits'
+import { liftTemporal2, pureTemporal, temporalAverage } from 'Engine/temporal'
import { inferUnit, serializeUnit } from 'Engine/units'
import { curry, map } from 'ramda'
import React from 'react'
-import { convertToDateIfNeeded } from '../date.ts'
+const comparisonOperator = ['≠', '=', '<', '>', '≤', '≥']
export default (k, operatorFunction, symbol) => (recurse, k, v) => {
let evaluate = (cache, situation, parsedRules, node) => {
const explanation = map(
@@ -43,30 +45,49 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => {
)
}
}
- let node1Value = node1.nodeValue
- let node2Value = node2.nodeValue
- try {
- ;[node1Value, node2Value] = convertToDateIfNeeded(
- node1.nodeValue,
- node2.nodeValue
- )
- } catch (e) {
- typeWarning(
- cache._meta.contextRule,
- `Impossible de convertir une des valeur en date`,
- e
- )
- }
- let nodeValue = operatorFunction(node1Value, node2Value)
-
- let unit = inferUnit(k, [node1.unit, node2.unit])
- return {
+ const baseNode = {
...node,
- nodeValue,
- unit,
explanation,
+ unit: inferUnit(k, [node1.unit, node2.unit]),
missingVariables
}
+
+ let temporalValue = liftTemporal2(
+ (a, b) => {
+ if (['∕', '-'].includes(node.operator) && a === false) {
+ return false
+ }
+ if (['+'].includes(node.operator) && a === false) {
+ return b
+ }
+ if (['∕', '-', '×', '+'].includes(node.operator) && b === false) {
+ return a
+ }
+ if (
+ !['=', '≠'].includes(node.operator) &&
+ (a === false || b === false)
+ ) {
+ return false
+ }
+ if (
+ comparisonOperator.includes(node.operator) &&
+ [a, b].every(value => value.match?.(/[\d]{2}\/[\d]{2}\/[\d]{4}/))
+ ) {
+ return operatorFunction(convertToDate(a), convertToDate(b))
+ }
+ return operatorFunction(a, b)
+ },
+ node1.temporalValue ?? pureTemporal(node1.nodeValue),
+ node2.temporalValue ?? pureTemporal(node2.nodeValue)
+ )
+
+ const nodeValue = temporalAverage(temporalValue, baseNode.unit)
+
+ return {
+ ...baseNode,
+ nodeValue,
+ ...(temporalValue.length > 1 && { temporalValue })
+ }
}
let explanation = v.explanation.map(recurse)
diff --git a/source/engine/mecanisms/régularisation.ts b/source/engine/mecanisms/régularisation.ts
new file mode 100644
index 000000000..8d9e74b86
--- /dev/null
+++ b/source/engine/mecanisms/régularisation.ts
@@ -0,0 +1,151 @@
+import { convertToString, getYear } from 'Engine/date'
+import { evaluationError } from 'Engine/error'
+import { evaluateNode } from 'Engine/evaluation'
+import {
+ createTemporalEvaluation,
+ Evaluation,
+ groupByYear,
+ liftTemporal2,
+ Temporal,
+ temporalAverage,
+ temporalCumul
+} from 'Engine/temporal'
+import { Unit } from 'Engine/units'
+import { DottedName } from 'Types/rule'
+import { coerceArray } from '../../utils'
+
+export default function parse(parse, k, v) {
+ const rule = parse(v.règle)
+ if (!v['valeurs cumulées']) {
+ throw new Error(
+ 'Il manque la clé `valeurs cumulées` dans le mécanisme régularisation'
+ )
+ }
+
+ const variables = coerceArray(v['valeurs cumulées']).map(parse) as Array<{
+ dottedName: DottedName
+ category: string
+ name: 'string'
+ }>
+ if (variables.some(({ category }) => category !== 'reference')) {
+ throw new Error(
+ 'Le mécanisme régularisation attend des noms de règles sous la clé `valeurs cumulées`'
+ )
+ }
+
+ return {
+ evaluate,
+ explanation: {
+ rule,
+ variables
+ },
+ category: 'mecanism',
+ name: 'taux progressif',
+ type: 'numeric',
+ unit: rule.unit
+ }
+}
+
+function getMonthlyCumulatedValuesOverYear(
+ year: number,
+ variable: Temporal>,
+ unit: Unit
+): Temporal> {
+ const start = convertToString(new Date(year, 0, 1))
+ const cumulatedPeriods = [...Array(12).keys()]
+ .map(monthNumber => ({
+ start,
+ end: convertToString(new Date(year, monthNumber + 1, 0))
+ }))
+ .map(period => {
+ const temporal = liftTemporal2(
+ (filter, value) => filter && value,
+ createTemporalEvaluation(true, period),
+ variable
+ )
+ return {
+ ...period,
+ value: temporalCumul(temporal, unit)
+ }
+ })
+
+ return cumulatedPeriods
+}
+
+function evaluate(
+ cache,
+ situation,
+ parsedRules,
+ node: ReturnType
+) {
+ const evaluate = evaluateNode.bind(null, cache, situation, parsedRules)
+
+ function recalculWith(situationGate: (dottedName: DottedName) => any, node) {
+ const newSituation = (dottedName: DottedName) =>
+ situationGate(dottedName) ?? situation(dottedName)
+ return evaluateNode({ _meta: cache._meta }, newSituation, parsedRules, node)
+ }
+
+ function regulariseYear(temporalEvaluation: Temporal>) {
+ if (temporalEvaluation.filter(({ value }) => value !== false).length <= 1) {
+ return temporalEvaluation
+ }
+
+ const currentYear = getYear(temporalEvaluation[0].start as string)
+ const cumulatedVariables = node.explanation.variables.reduce(
+ (acc, parsedVariable) => {
+ const evaluation = evaluate(parsedVariable)
+ if (!evaluation.temporalValue) {
+ evaluationError(
+ cache._meta.contextRule,
+ `Dans le mécanisme régularisation, la valeur annuelle ${parsedVariable.name} n'est pas une variables temporelle`
+ )
+ }
+ return {
+ ...acc,
+ [parsedVariable.dottedName]: getMonthlyCumulatedValuesOverYear(
+ currentYear,
+ evaluation.temporalValue,
+ evaluation.unit
+ )
+ }
+ },
+ {}
+ )
+
+ const cumulatedMonthlyEvaluations = [...Array(12).keys()].map(i => ({
+ start: convertToString(new Date(currentYear, i, 1)),
+ end: convertToString(new Date(currentYear, i + 1, 0)),
+ value: recalculWith(
+ dottedName => cumulatedVariables[dottedName]?.[i].value,
+ node.explanation.rule
+ ).nodeValue
+ }))
+ const temporalRégularisée = cumulatedMonthlyEvaluations.map(
+ (period, i) => ({
+ ...period,
+ value: period.value - (cumulatedMonthlyEvaluations[i - 1]?.value ?? 0)
+ })
+ )
+
+ return temporalRégularisée as Temporal>
+ }
+
+ const evaluation = evaluate(node.explanation.rule)
+
+ const temporalValue = evaluation.temporalValue
+ const evaluationWithRegularisation = groupByYear(
+ temporalValue as Temporal>
+ )
+ .map(regulariseYear)
+ .flat()
+
+ return {
+ ...node,
+ temporalValue: evaluationWithRegularisation,
+ explanation: evaluation,
+ nodeValue: temporalAverage(temporalValue),
+ missingVariables: evaluation.missingVariables,
+ unit: evaluation.unit
+ }
+}
diff --git a/source/engine/mecanisms/trancheUtils.ts b/source/engine/mecanisms/trancheUtils.ts
index f0e493316..a0032d3af 100644
--- a/source/engine/mecanisms/trancheUtils.ts
+++ b/source/engine/mecanisms/trancheUtils.ts
@@ -1,4 +1,5 @@
import { mergeAllMissing } from 'Engine/evaluation'
+import { Evaluation } from 'Engine/temporal'
import { evolve } from 'ramda'
import { evaluationError, typeWarning } from '../error'
import { convertUnit, inferUnit } from '../units'
@@ -7,7 +8,6 @@ export const parseTranches = (parse, tranches) => {
return tranches
.map((t, i) => {
if (!t.plafond && i > tranches.length) {
- console.log(t, i)
throw new SyntaxError(
`La tranche n°${i} du barème n'a pas de plafond précisé. Seule la dernière tranche peut ne pas être plafonnée`
)
@@ -32,35 +32,30 @@ export function evaluatePlafondUntilActiveTranche(
}
const plafond = evaluate(parsedTranche.plafond)
+ if (plafond.temporalValue) {
+ evaluationError(
+ cache._meta.contextRule,
+ 'Les valeurs temporelles ne sont pas acceptées pour un plafond de tranche'
+ )
+ }
const plancher = tranches[i - 1]
? tranches[i - 1].plafond
: { nodeValue: 0 }
- const calculationValues = [plafond, assiette, multiplicateur, plancher]
- if (calculationValues.some(node => node.nodeValue === null)) {
- return [
- [
- ...tranches,
- {
- ...parsedTranche,
- plafond,
- nodeValue: null,
- isActive: null,
- isAfterActive: false,
- missingVariables: mergeAllMissing(calculationValues)
- }
- ],
- false
- ]
- }
- let plafondValue = plafond.nodeValue * multiplicateur.nodeValue
+
+ let plafondValue: Evaluation =
+ plafond.nodeValue === null || multiplicateur.nodeValue === null
+ ? null
+ : plafond.nodeValue * multiplicateur.nodeValue
+
try {
- plafondValue = [Infinity || 0].includes(plafondValue)
- ? plafondValue
- : convertUnit(
- inferUnit('*', [plafond.unit, multiplicateur.unit]),
- assiette.unit,
- plafondValue
- )
+ plafondValue =
+ plafondValue === Infinity || plafondValue === 0
+ ? plafondValue
+ : convertUnit(
+ inferUnit('*', [plafond.unit, multiplicateur.unit]),
+ assiette.unit,
+ plafondValue
+ )
} catch (e) {
typeWarning(
cache._meta.contextRule,
@@ -69,9 +64,37 @@ export function evaluatePlafondUntilActiveTranche(
e
)
}
-
let plancherValue = tranches[i - 1] ? tranches[i - 1].plafondValue : 0
- if (!!tranches[i - 1] && plafondValue <= plancherValue) {
+ const isAfterActive =
+ plancherValue === null || assiette.nodeValue === null
+ ? null
+ : plancherValue > assiette.nodeValue
+
+ const calculationValues = [plafond, assiette, multiplicateur, plancher]
+ if (calculationValues.some(node => node.nodeValue === null)) {
+ return [
+ [
+ ...tranches,
+ {
+ ...parsedTranche,
+ plafond,
+ plafondValue,
+ plancherValue,
+ nodeValue: null,
+ isActive: null,
+ isAfterActive,
+ missingVariables: mergeAllMissing(calculationValues)
+ }
+ ],
+ false
+ ]
+ }
+
+ if (
+ !!tranches[i - 1] &&
+ !!plancherValue &&
+ plafondValue <= plancherValue
+ ) {
evaluationError(
cache._meta.contextRule,
`Le plafond de la tranche n°${i +
@@ -84,10 +107,10 @@ export function evaluatePlafondUntilActiveTranche(
plafond,
plancherValue,
plafondValue,
- isAfterActive: false,
+ isAfterActive,
isActive:
assiette.nodeValue >= plancherValue &&
- assiette.nodeValue < plafondValue
+ assiette.nodeValue < plafondValue
}
return [[...tranches, tranche], tranche.isActive]
diff --git a/source/engine/mecanisms/variableTemporelle.ts b/source/engine/mecanisms/variableTemporelle.ts
new file mode 100644
index 000000000..80f09383b
--- /dev/null
+++ b/source/engine/mecanisms/variableTemporelle.ts
@@ -0,0 +1,63 @@
+import { evaluateNode } from 'Engine/evaluation'
+import {
+ createTemporalEvaluation,
+ narrowTemporalValue,
+ temporalAverage
+} from 'Engine/temporal'
+import { Temporal } from './../temporal'
+
+function evaluate(
+ cache: any,
+ situation: any,
+ parsedRules: any,
+ node: ReturnType
+) {
+ const evaluateAttribute = evaluateNode.bind(
+ null,
+ cache,
+ situation,
+ parsedRules
+ )
+
+ const start =
+ node.explanation.period.start &&
+ evaluateAttribute(node.explanation.period.start)
+ const end =
+ node.explanation.period.end &&
+ evaluateAttribute(node.explanation.period.end)
+ const value = evaluateAttribute(node.explanation.value)
+ const period = {
+ start: start?.nodeValue ?? null,
+ end: end?.nodeValue ?? null
+ }
+
+ const temporalValue = value.temporalValue
+ ? narrowTemporalValue(period, value.temporalValue)
+ : createTemporalEvaluation(value.nodeValue, period)
+ // TODO explanation missingVariables / period missing variables
+ return {
+ ...node,
+ nodeValue: temporalAverage(temporalValue as Temporal, value.unit),
+ temporalValue,
+ explanation: {
+ period: { start, end },
+ value
+ },
+ unit: value.unit
+ }
+}
+
+export default function parseVariableTemporelle(parse, __, v: any) {
+ const explanation = parse(v.explanation)
+ return {
+ evaluate,
+ explanation: {
+ period: {
+ start: v.period.start && parse(v.period.start),
+ end: v.period.end && parse(v.period.end)
+ },
+ value: explanation
+ },
+ unit: explanation.unit
+ }
+}
diff --git a/source/engine/mecanisms/variations.js b/source/engine/mecanisms/variations.js
deleted file mode 100644
index 127e28ac3..000000000
--- a/source/engine/mecanisms/variations.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import {
- bonus,
- collectNodeMissing,
- evaluateNode,
- mergeAllMissing,
- mergeMissing
-} from 'Engine/evaluation'
-import Variations from 'Engine/mecanismViews/Variations'
-import { inferUnit } from 'Engine/units'
-import { dissoc, filter, isNil, pluck, reduce, reject } from 'ramda'
-
-/* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */
-export default (recurse, k, v, devariate) => {
- let explanation = devariate
- ? devariateExplanation(recurse, k, v)
- : v.map(({ si, alors, sinon }) =>
- sinon !== undefined
- ? { consequence: recurse(sinon), condition: undefined }
- : { consequence: recurse(alors), condition: recurse(si) }
- )
-
- let evaluate = (cache, situationGate, parsedRules, node) => {
- let evaluateVariationProp = prop =>
- prop && evaluateNode(cache, situationGate, parsedRules, prop),
- // mark the satisfied variation if any in the explanation
- [, resolvedExplanation] = reduce(
- ([resolved, result], variation) => {
- if (resolved) return [true, [...result, variation]]
-
- // evaluate the condition
- let evaluatedCondition = evaluateVariationProp(variation.condition)
-
- if (evaluatedCondition == undefined) {
- // No condition : we've reached the eventual defaut case
- let evaluatedVariation = {
- consequence: evaluateVariationProp(variation.consequence),
- satisfied: true
- }
- return [true, [...result, evaluatedVariation]]
- }
-
- if (evaluatedCondition.nodeValue === null)
- // the current variation case has missing variables => we can't go further
- return [
- true,
- [...result, { ...variation, condition: evaluatedCondition }]
- ]
-
- if (evaluatedCondition.nodeValue === true) {
- let evaluatedVariation = {
- condition: evaluatedCondition,
- consequence: evaluateVariationProp(variation.consequence),
- satisfied: true
- }
- return [true, [...result, evaluatedVariation]]
- }
- return [false, [...result, variation]]
- },
- [false, []]
- )(node.explanation),
- satisfiedVariation = resolvedExplanation.find(v => v.satisfied),
- nodeValue = satisfiedVariation
- ? satisfiedVariation.consequence.nodeValue
- : null
-
- let leftMissing = mergeAllMissing(
- reject(isNil, pluck('condition', resolvedExplanation))
- ),
- candidateVariations = filter(
- node => !node.condition || node.condition.nodeValue !== false,
- resolvedExplanation
- ),
- rightMissing = mergeAllMissing(
- reject(isNil, pluck('consequence', candidateVariations))
- ),
- missingVariables = satisfiedVariation
- ? collectNodeMissing(satisfiedVariation.consequence)
- : mergeMissing(bonus(leftMissing), rightMissing)
-
- return {
- ...node,
- nodeValue,
- ...(satisfiedVariation && { unit: satisfiedVariation?.consequence.unit }),
- explanation: resolvedExplanation,
- missingVariables
- }
- }
-
- // TODO - find an appropriate representation
- return {
- explanation,
- evaluate,
- jsx: Variations,
- category: 'mecanism',
- name: 'variations',
- type: 'numeric',
- unit: inferUnit(
- '+',
- explanation.map(r => r.consequence.unit)
- )
- }
-}
-
-export let devariateExplanation = (recurse, mecanismKey, v) => {
- let fixedProps = dissoc('variations')(v),
- explanation = v.variations.map(({ si, alors, sinon }) => ({
- consequence: recurse({
- [mecanismKey]: {
- ...fixedProps,
- ...(sinon || alors)
- }
- }),
- condition: sinon ? undefined : recurse(si)
- }))
-
- return explanation
-}
diff --git a/source/engine/mecanisms/variations.ts b/source/engine/mecanisms/variations.ts
new file mode 100644
index 000000000..83aa46265
--- /dev/null
+++ b/source/engine/mecanisms/variations.ts
@@ -0,0 +1,164 @@
+import { typeWarning } from 'Engine/error'
+import { defaultNode, evaluateNode } from 'Engine/evaluation'
+import Variations from 'Engine/mecanismViews/Variations'
+import { convertNodeToUnit } from 'Engine/nodeUnits'
+import {
+ liftTemporal2,
+ pureTemporal,
+ sometime,
+ temporalAverage
+} from 'Engine/temporal'
+import { inferUnit } from 'Engine/units'
+import { or } from 'ramda'
+import { mergeAllMissing } from './../evaluation'
+
+/* @devariate = true => This function will produce variations of a same mecanism (e.g. product) that share some common properties */
+export default function parse(recurse, k, v, devariate) {
+ let explanation = devariate
+ ? devariateExplanation(recurse, k, v)
+ : v.map(({ si, alors, sinon }) =>
+ sinon !== undefined
+ ? { consequence: recurse(sinon), condition: defaultNode(true) }
+ : { consequence: recurse(alors), condition: recurse(si) }
+ )
+
+ // TODO - find an appropriate representation
+ return {
+ explanation,
+ evaluate,
+ jsx: Variations,
+ category: 'mecanism',
+ name: 'variations',
+ type: 'numeric',
+ unit: inferUnit(
+ '+',
+ explanation.map(r => r.consequence.unit)
+ )
+ }
+}
+type Variation =
+ | {
+ si: any
+ alors: Object
+ }
+ | {
+ sinon: Object
+ }
+export let devariateExplanation = (
+ recurse,
+ mecanismKey,
+ v: { variations: Array }
+) => {
+ const { variations, ...fixedProps } = v
+ const explanation = variations.map(variation => ({
+ condition: 'sinon' in variation ? defaultNode(true) : recurse(variation.si),
+ consequence: recurse({
+ [mecanismKey]: {
+ ...fixedProps,
+ ...('sinon' in variation ? variation.sinon : variation.alors)
+ }
+ })
+ }))
+
+ return explanation
+}
+
+function evaluate(
+ cache,
+ situationGate,
+ parsedRules,
+ node: ReturnType
+) {
+ const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
+
+ const [temporalValue, explanation, unit] = node.explanation.reduce(
+ (
+ [evaluation, explanations, unit, previousConditions],
+ { condition, consequence },
+ i: number
+ ) => {
+ const previousConditionsAlwaysTrue = !sometime(
+ value => value !== true,
+ previousConditions
+ )
+ if (previousConditionsAlwaysTrue) {
+ return [
+ evaluation,
+ [...explanations, { condition, consequence }],
+ unit,
+ previousConditions
+ ]
+ }
+ const evaluatedCondition = evaluate(condition)
+ const currentCondition = liftTemporal2(
+ (previousCond, currentCond) =>
+ previousCond === null ? previousCond : !previousCond && currentCond,
+ previousConditions,
+ evaluatedCondition.temporalValue ??
+ pureTemporal(evaluatedCondition.nodeValue)
+ )
+ const currentConditionAlwaysFalse = !sometime(
+ x => x !== false,
+ currentCondition
+ )
+ if (currentConditionAlwaysFalse) {
+ return [
+ evaluation,
+ [...explanations, { condition: evaluatedCondition, consequence }],
+ unit,
+ previousConditions
+ ]
+ }
+ let evaluatedConsequence = evaluate(consequence)
+
+ try {
+ evaluatedConsequence = convertNodeToUnit(unit, evaluatedConsequence)
+ } catch (e) {
+ return typeWarning(
+ cache._meta.contexRule,
+ `L'unité de la branche n° ${i} du mécanisme 'variations' n'est pas compatible avec celle d'une branche précédente`,
+ e
+ )
+ }
+ const currentValue = liftTemporal2(
+ (cond, value) => cond && value,
+ currentCondition,
+ evaluatedConsequence.temporalValue ??
+ pureTemporal(evaluatedConsequence.nodeValue)
+ )
+ return [
+ liftTemporal2(or, evaluation, currentValue),
+ [
+ ...explanations,
+ {
+ condition: evaluatedCondition,
+ satisfied: !!evaluatedCondition.nodeValue,
+ consequence: evaluatedConsequence
+ }
+ ],
+ unit || evaluatedConsequence.unit,
+ liftTemporal2(or, previousConditions, currentCondition)
+ ]
+ },
+ [pureTemporal(false), [], node.unit, pureTemporal(false)]
+ )
+ const nodeValue = temporalAverage(temporalValue, unit)
+ const missingVariables = mergeAllMissing(
+ explanation.reduce(
+ (values, { condition, consequence }) => [
+ ...values,
+ condition,
+ consequence
+ ],
+ []
+ )
+ )
+ return {
+ ...node,
+ nodeValue,
+ unit,
+ explanation,
+ missingVariables,
+ ...(temporalValue.length > 1 && { temporalValue })
+ }
+}
diff --git a/source/engine/nodeUnits.ts b/source/engine/nodeUnits.ts
index c0ef9c773..b41e669e7 100644
--- a/source/engine/nodeUnits.ts
+++ b/source/engine/nodeUnits.ts
@@ -1,3 +1,4 @@
+import { EvaluatedNode, mapTemporal } from './temporal'
import {
areUnitConvertible,
convertUnit,
@@ -6,10 +7,11 @@ import {
} from './units'
export function simplifyNodeUnit(node) {
- if (!node.unit || !node.nodeValue) {
+ if (!node.unit || node.nodeValue === false || node.nodeValue == null) {
return node
}
const [unit, nodeValue] = simplifyUnitWithValue(node.unit, node.nodeValue)
+
return {
...node,
unit,
@@ -35,12 +37,19 @@ export const getNodeDefaultUnit = (node, cache) => {
)
}
-export function convertNodeToUnit(to: Unit, node) {
+export function convertNodeToUnit(to: Unit, node: EvaluatedNode) {
return {
...node,
nodeValue: node.unit
? convertUnit(node.unit, to, node.nodeValue)
: node.nodeValue,
+ temporalValue:
+ node.temporalValue && node.unit
+ ? mapTemporal(
+ value => convertUnit(node.unit, to, value),
+ node.temporalValue
+ )
+ : node.temporalValue,
unit: to
}
}
diff --git a/source/engine/parse.tsx b/source/engine/parse.tsx
index 1a7734f0f..0fe14112d 100644
--- a/source/engine/parse.tsx
+++ b/source/engine/parse.tsx
@@ -9,7 +9,9 @@ import durée from 'Engine/mecanisms/durée'
import encadrement from 'Engine/mecanisms/encadrement'
import grille from 'Engine/mecanisms/grille'
import operation from 'Engine/mecanisms/operation'
+import régularisation from 'Engine/mecanisms/régularisation'
import tauxProgressif from 'Engine/mecanisms/tauxProgressif'
+import variableTemporelle from 'Engine/mecanisms/variableTemporelle'
import variations from 'Engine/mecanisms/variations'
import { Grammar, Parser } from 'nearley'
import {
@@ -25,7 +27,7 @@ import {
subtract
} from 'ramda'
import React from 'react'
-import { syntaxError } from './error'
+import { EngineError, syntaxError } from './error'
import grammar from './grammar.ne'
import {
mecanismAllOf,
@@ -86,6 +88,17 @@ export const parseExpression = (rule, rawNode) => {
}
const parseMecanism = (rules, rule, parsedRules) => rawNode => {
+ if (Array.isArray(rawNode)) {
+ syntaxError(
+ rule.dottedName,
+ `
+Il manque le nom du mécanisme pour le tableau : [${rawNode
+ .map(x => `'${x}'`)
+ .join(', ')}]
+Les mécanisme possibles sont : 'somme', 'le maximum de', 'le minimum de', 'toutes ces conditions', 'une de ces conditions'.
+ `
+ )
+ }
if (Object.keys(rawNode).length > 1) {
syntaxError(
rule.dottedName,
@@ -136,7 +149,14 @@ Le mécanisme ${mecanismName} est inconnu.
Vérifiez qu'il n'y ait pas d'erreur dans l'orthographe du nom.`
)
}
- return parseFn(parse(rules, rule, parsedRules), mecanismName, values)
+ try {
+ return parseFn(parse(rules, rule, parsedRules), mecanismName, values)
+ } catch (e) {
+ if (e instanceof EngineError) {
+ throw e
+ }
+ syntaxError(rule.dottedName, e.message)
+ }
}
const knownOperations = {
@@ -164,8 +184,10 @@ const statelessParseFunction = {
'une de ces conditions': mecanismOneOf,
'toutes ces conditions': mecanismAllOf,
somme: mecanismSum,
+ régularisation,
multiplication: mecanismProduct,
produit: mecanismProduct,
+ temporalValue: variableTemporelle,
arrondi: mecanismRound,
barème,
grille,
diff --git a/source/engine/parseReference.js b/source/engine/parseReference.js
index aad038595..8f926be86 100644
--- a/source/engine/parseReference.js
+++ b/source/engine/parseReference.js
@@ -149,10 +149,11 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v
if (cached) return addReplacementMissingVariable(cached)
- let cacheNode = (nodeValue, missingVariables, explanation) => {
+ let cacheNode = (nodeValue, missingVariables, explanation, temporalValue) => {
cache[cacheName] = {
...node,
nodeValue,
+ temporalValue,
...(explanation && {
explanation
}),
@@ -183,7 +184,8 @@ Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, v
return cacheNode(
evaluation.nodeValue,
evaluation.missingVariables,
- evaluation
+ evaluation,
+ evaluation.temporalValue
)
}
diff --git a/source/engine/parseRule.tsx b/source/engine/parseRule.tsx
index 67dbcbd90..bfdb7ab0d 100644
--- a/source/engine/parseRule.tsx
+++ b/source/engine/parseRule.tsx
@@ -83,8 +83,16 @@ export default (rules, rule, parsedRules) => {
parsedRules,
node.explanation
),
- { nodeValue, unit, missingVariables } = explanation
- return { ...node, nodeValue, unit, missingVariables, explanation }
+ { nodeValue, unit, missingVariables, temporalValue } = explanation
+
+ return {
+ ...node,
+ nodeValue,
+ unit,
+ missingVariables,
+ explanation,
+ temporalValue
+ }
}
let child = parse(rules, rule, parsedRules)(value)
@@ -129,8 +137,8 @@ export default (rules, rule, parsedRules) => {
...parsedRoot,
evaluate,
parsed: true,
- isDisabledBy: [],
defaultUnit: parsedRoot.defaultUnit || parsedRoot.formule?.unit,
+ isDisabledBy: [],
replacedBy: []
}
parsedRules[rule.dottedName]['rendu non applicable'] = {
@@ -147,6 +155,7 @@ export default (rules, rule, parsedRules) => {
missingVariables: mergeAllMissing(isDisabledBy)
}
},
+
jsx: (_nodeValue, { isDisabledBy }) => {
return (
isDisabledBy.length > 0 && (
diff --git a/source/engine/rules.js b/source/engine/rules.js
index 83588f66a..0df4a87eb 100644
--- a/source/engine/rules.js
+++ b/source/engine/rules.js
@@ -1,6 +1,30 @@
import { parseUnit } from 'Engine/units'
import rawRules from 'Publicode/rules'
-import { assoc, chain, dropLast, filter, fromPairs, is, isNil, join, last, map, path, pipe, propEq, props, range, reduce, reduced, reject, split, take, toPairs, trim, when } from 'ramda'
+import {
+ assoc,
+ chain,
+ dropLast,
+ filter,
+ fromPairs,
+ is,
+ isNil,
+ join,
+ last,
+ map,
+ path,
+ pipe,
+ propEq,
+ props,
+ range,
+ reduce,
+ reduced,
+ reject,
+ split,
+ take,
+ toPairs,
+ trim,
+ when
+} from 'ramda'
import translations from '../locales/rules-en.yaml'
// TODO - should be in UI, not engine
import { capitalise0, coerceArray } from '../utils'
@@ -89,7 +113,7 @@ export let ruleParents = dottedName => {
*/
export let disambiguateRuleReference = (
allRules,
- { dottedName, name },
+ { dottedName },
partialName
) => {
let pathPossibilities = [
@@ -113,9 +137,8 @@ export let disambiguateRuleReference = (
return found.dottedName
}
- throw new Error(
- `OUUUUPS la référence '${partialName}' dans la règle '${name}' est introuvable dans la base`
- )
+ throw new Error(`La référence '${partialName}' est introuvable.
+Vérifiez que l'orthographe et l'espace de nom sont corrects`)
}
export let collectDefaults = pipe(
diff --git a/source/engine/temporal.ts b/source/engine/temporal.ts
new file mode 100644
index 000000000..f307198e3
--- /dev/null
+++ b/source/engine/temporal.ts
@@ -0,0 +1,438 @@
+import {
+ convertToDate,
+ getDifferenceInDays,
+ getDifferenceInMonths,
+ getDifferenceInYears,
+ getRelativeDate,
+ getYear
+} from 'Engine/date'
+import { Unit } from './units'
+
+export type Period = {
+ start: T | null
+ end: T | null
+}
+
+export function parsePeriod(word: string, date: Date): Period {
+ const startWords = [
+ 'depuis',
+ 'depuis le',
+ 'depuis la',
+ 'à partir de',
+ 'à partir du',
+ 'du'
+ ]
+ const endWords = [
+ "jusqu'à",
+ "jusqu'au",
+ "jusqu'à la",
+ 'avant',
+ 'avant le',
+ 'avant la',
+ 'au'
+ ]
+ const intervalWords = ['le', 'en']
+ if (!startWords.concat(endWords, intervalWords).includes(word)) {
+ throw new SyntaxError(
+ `Le mot clé '${word}' n'est pas valide. Les mots clés possible sont les suivants :\n\t ${startWords.join(
+ ', '
+ )}`
+ )
+ }
+ if (word === 'le') {
+ return {
+ start: date,
+ end: date
+ }
+ }
+ if (word === 'en') {
+ return { start: null, end: null }
+ }
+ if (startWords.includes(word)) {
+ return {
+ start: date,
+ end: null
+ }
+ }
+ if (endWords.includes(word)) {
+ return {
+ start: null,
+ end: date
+ }
+ }
+ throw new Error('Non implémenté')
+}
+
+// Idée : une évaluation est un n-uple : (value, unit, missingVariable, isApplicable)
+// Une temporalEvaluation est une liste d'evaluation sur chaque période. : [(Evaluation, Period)]
+export type Evaluation = T | false | null
+
+export type EvaluatedNode = {
+ unit: Unit
+ nodeValue: Evaluation
+ temporalValue?: Temporal>
+ explanation?: Object
+ missingVariables?: Object
+}
+
+export type TemporalNode = Temporal<{ nodeValue: Evaluation }>
+export type Temporal = Array & { value: T }>
+
+export function narrowTemporalValue(
+ period: Period,
+ temporalValue: Temporal>
+): Temporal> {
+ return liftTemporal2(
+ (value, filter) => filter && value,
+ temporalValue,
+ createTemporalEvaluation(true, period)
+ )
+}
+
+// Returns a temporal value that's true for the given period and false otherwise.
+export function createTemporalEvaluation(
+ value: Evaluation,
+ period: Period = { start: null, end: null }
+): Temporal> {
+ let temporalValue = [{ ...period, value }]
+ if (period.start != null) {
+ temporalValue.unshift({
+ start: null,
+ end: getRelativeDate(period.start, -1),
+ value: false
+ })
+ }
+ if (period.end != null) {
+ temporalValue.push({
+ start: getRelativeDate(period.end, 1),
+ end: null,
+ value: false
+ })
+ }
+ return temporalValue
+}
+
+export function pureTemporal(value: T): Temporal {
+ return [{ start: null, end: null, value }]
+}
+
+export function mapTemporal(
+ fn: (value: T1) => T2,
+ temporalValue: Temporal
+): Temporal {
+ return temporalValue.map(({ start, end, value }) => ({
+ start,
+ end,
+ value: fn(value)
+ }))
+}
+export function sometime(
+ fn: (value: T1) => boolean,
+ temporalValue: Temporal
+): boolean {
+ return temporalValue.some(({ start, end, value }) => fn(value))
+}
+
+export function liftTemporal2(
+ fn: (value1: T1, value2: T2) => T3,
+ temporalValue1: Temporal,
+ temporalValue2: Temporal
+): Temporal {
+ return mapTemporal(
+ ([a, b]) => fn(a, b),
+ zipTemporals(temporalValue1, temporalValue2)
+ )
+}
+
+export function concatTemporals(
+ temporalValues: Array>
+): Temporal> {
+ return temporalValues.reduce(
+ (values, value) => liftTemporal2((a, b) => [...a, b], values, value),
+ pureTemporal([]) as Temporal>
+ )
+}
+
+export function liftTemporalNode(node: EvaluatedNode): TemporalNode {
+ const { temporalValue, ...baseNode } = node
+ if (!temporalValue) {
+ return pureTemporal(baseNode)
+ }
+ return mapTemporal(
+ nodeValue => ({
+ ...baseNode,
+ nodeValue
+ }),
+ temporalValue
+ )
+}
+
+export function zipTemporals(
+ temporalValue1: Temporal,
+ temporalValue2: Temporal,
+ acc: Temporal<[T1, T2]> = []
+): Temporal<[T1, T2]> {
+ if (!temporalValue1.length && !temporalValue2.length) {
+ return acc
+ }
+ const [value1, ...rest1] = temporalValue1
+ const [value2, ...rest2] = temporalValue2
+ console.assert(value1.start === value2.start)
+ const endDateComparison = compareEndDate(value1.end, value2.end)
+
+ // End dates are equals
+ if (endDateComparison === 0) {
+ return zipTemporals(rest1, rest2, [
+ ...acc,
+ { ...value1, value: [value1.value, value2.value] }
+ ])
+ }
+ // Value1 lasts longuer than value1
+ if (endDateComparison > 0) {
+ console.assert(value2.end !== null)
+ return zipTemporals(
+ [
+ { ...value1, start: getRelativeDate(value2.end as string, 1) },
+ ...rest1
+ ],
+ rest2,
+ [
+ ...acc,
+ {
+ ...value2,
+ value: [value1.value, value2.value]
+ }
+ ]
+ )
+ }
+
+ // Value2 lasts longuer than value1
+ if (endDateComparison < 0) {
+ console.assert(value1.end !== null)
+ return zipTemporals(
+ rest1,
+ [
+ { ...value2, start: getRelativeDate(value1.end as string, 1) },
+ ...rest2
+ ],
+ [
+ ...acc,
+ {
+ ...value1,
+ value: [value1.value, value2.value]
+ }
+ ]
+ )
+ }
+ throw new EvalError('All case should have been covered')
+}
+
+function beginningOfNextYear(date: string): string {
+ return `01/01/${getYear(date) + 1}`
+}
+
+function endsOfPreviousYear(date: string): string {
+ return `31/12/${getYear(date) - 1}`
+}
+
+function splitStartsAt(
+ fn: (date: string) => string,
+ temporal: Temporal
+): Temporal {
+ return temporal.reduce((acc, period) => {
+ const { start, end } = period
+ const newStart = start === null ? start : fn(start)
+ if (compareEndDate(newStart, end) !== -1) {
+ return [...acc, period]
+ }
+ console.assert(newStart !== null)
+ return [
+ ...acc,
+ { ...period, end: getRelativeDate(newStart as string, -1) },
+ { ...period, start: newStart }
+ ]
+ }, [] as Temporal)
+}
+
+function splitEndsAt(
+ fn: (date: string) => string,
+ temporal: Temporal
+): Temporal {
+ return temporal.reduce((acc, period) => {
+ const { start, end } = period
+ const newEnd = end === null ? end : fn(end)
+ if (compareStartDate(start, newEnd) !== -1) {
+ return [...acc, period]
+ }
+ console.assert(newEnd !== null)
+ return [
+ ...acc,
+ { ...period, end: newEnd },
+ { ...period, start: getRelativeDate(newEnd as string, 1) }
+ ]
+ }, [] as Temporal)
+}
+
+export function groupByYear(temporalValue: Temporal): Array> {
+ return (
+ // First step: split period by year if needed
+ splitEndsAt(
+ endsOfPreviousYear,
+ splitStartsAt(beginningOfNextYear, temporalValue)
+ )
+ // Second step: group period by year
+ .reduce((acc, period) => {
+ const [currentTemporal, ...otherTemporal] = acc
+ if (currentTemporal === undefined) {
+ return [[period]]
+ }
+ const firstPeriod = currentTemporal[0]
+ console.assert(
+ firstPeriod !== undefined &&
+ firstPeriod.end !== null &&
+ period.start !== null,
+ 'invariant non verifié'
+ )
+ if (
+ (firstPeriod.end as string).slice(-4) !==
+ (period.start as string).slice(-4)
+ ) {
+ return [[period], ...acc]
+ }
+ return [[...currentTemporal, period], ...otherTemporal]
+ }, [] as Array>)
+ .reverse()
+ )
+}
+
+function simplify(temporalValue: Temporal): Temporal {
+ return temporalValue
+}
+
+function compareStartDate(
+ dateA: string | null,
+ dateB: string | null
+): -1 | 0 | 1 {
+ if (dateA == dateB) {
+ return 0
+ }
+ if (dateA == null) {
+ return -1
+ }
+ if (dateB == null) {
+ return 1
+ }
+ return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1
+}
+
+function compareEndDate(
+ dateA: string | null,
+ dateB: string | null
+): -1 | 0 | 1 {
+ if (dateA == dateB) {
+ return 0
+ }
+ if (dateA == null) {
+ return 1
+ }
+ if (dateB == null) {
+ return -1
+ }
+ return convertToDate(dateA) < convertToDate(dateB) ? -1 : 1
+}
+
+export function temporalAverage(
+ temporalValue: Temporal>,
+ unit?: Unit
+): Evaluation {
+ temporalValue = temporalValue.filter(({ value }) => value !== false)
+ if (!temporalValue.length) {
+ return false
+ }
+ if (temporalValue.length === 1) {
+ return temporalValue[0].value
+ }
+
+ if (temporalValue.some(({ value }) => value == null)) {
+ return null
+ }
+
+ const temporalNumber = temporalValue as Temporal
+ const first = temporalNumber[0]
+ const last = temporalNumber[temporalNumber.length - 1]
+
+ // La variable est définie sur un interval infini
+ if (first.start == null || last.end == null) {
+ if (first.start != null) {
+ return last.value
+ }
+ if (last.end != null) {
+ return first.value
+ }
+ return (first.value + last.value) / 2
+ }
+
+ let totalWeight = 0
+ const weights = temporalNumber.map(({ start, end, value }) => {
+ ;[start, end] = [start, end] as [string, string]
+ let weight = 0
+ if (unit?.denominators.includes('mois')) {
+ weight = getDifferenceInMonths(start, end)
+ } else if (unit?.denominators.includes('année')) {
+ weight = getDifferenceInYears(start, end)
+ } else {
+ weight = getDifferenceInDays(start, end)
+ }
+ totalWeight += weight
+ return value * weight
+ })
+ return weights.reduce(
+ (average, weightedValue) => average + weightedValue / totalWeight,
+ 0
+ )
+}
+
+export function temporalCumul(
+ temporalValue: Temporal>,
+ unit: Unit
+): Evaluation {
+ temporalValue = temporalValue.filter(({ value }) => value !== false)
+ if (!temporalValue.length) {
+ return false
+ }
+
+ if (temporalValue.some(({ value }) => value == null)) {
+ return null
+ }
+
+ const temporalNumber = temporalValue as Temporal
+ const first = temporalNumber[0]
+ const last = temporalNumber[temporalNumber.length - 1]
+
+ // La variable est définie sur un interval infini
+ if (first.start == null || last.end == null) {
+ if (first.start != null) {
+ return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity
+ }
+ if (last.end != null) {
+ return !last.value ? last.value : last.value > 0 ? Infinity : -Infinity
+ }
+ return null
+ }
+ if (temporalNumber.some(({ value }) => value == null)) {
+ return null
+ }
+
+ return temporalNumber.reduce((acc, { start, end, value }) => {
+ ;[start, end] = [start, end] as [string, string]
+ let weight = 1
+ if (unit?.denominators.includes('mois')) {
+ weight = getDifferenceInMonths(start, end)
+ } else if (unit?.denominators.includes('année')) {
+ weight = getDifferenceInYears(start, end)
+ } else if (unit?.denominators.includes('jour')) {
+ weight = getDifferenceInDays(start, end)
+ }
+ return value * weight + acc
+ }, 0)
+}
diff --git a/source/engine/units.ts b/source/engine/units.ts
index 0d40601c9..f6b146774 100644
--- a/source/engine/units.ts
+++ b/source/engine/units.ts
@@ -12,6 +12,7 @@ import {
without
} from 'ramda'
import i18n from '../i18n'
+import { Evaluation } from './temporal'
type BaseUnit = string
@@ -186,7 +187,13 @@ export function convertUnit(
from: Unit | undefined,
to: Unit | undefined,
value: number
-) {
+): number
+export function convertUnit(
+ from: Unit | undefined,
+ to: Unit | undefined,
+ value: Evaluation
+): Evaluation
+export function convertUnit(from, to, value): any {
if (!areUnitConvertible(from, to)) {
throw new Error(
`Impossible de convertir l'unité '${serializeUnit(
diff --git a/source/locales/rules-en.yaml b/source/locales/rules-en.yaml
index 3f38fc666..02ba489d6 100644
--- a/source/locales/rules-en.yaml
+++ b/source/locales/rules-en.yaml
@@ -4207,8 +4207,39 @@ dirigeant . indépendant . revenu net de cotisations:
titre.en: net contribution income
titre.fr: revenu net de cotisations
dirigeant . indépendant . revenu professionnel:
- titre.en: '[automatic] occupational income'
- titre.fr: revenu professionnel
+ description.en: >
+ [automatic] This is the net deductible contribution income of the
+ self-employed person, which is used as the basis for the calculation of
+ contributions and tax for self-employed persons.
+
+
+ Attention, **our calculation is made at cruising speed**:
+
+ the self-employed person who starts out will pay a relatively small package
+ of social security contributions for the first 2 years. He will then have to
+ regularise this situation in relation to the income he has actually
+ received.
+
+
+ Therefore, this calculation should be seen as "the amount that will have to
+ be paid* in the short term after 2 years of exercise.
+ description.fr: >
+ C'est le revenu net de cotisations déductibles du travailleur indépendant,
+ qui sert de base au calcul des cotisations et de l'impôt pour les
+ indépendants.
+
+
+ Attention, **notre calcul est fait au régime de croisière**:
+
+ l'indépendant qui se lance paiera pendant ses 2 premières années un forfait
+ relativement réduit de cotisations sociales. Il devra ensuite régulariser
+ cette situation par rapport au revenu qu'il a vraiment perçu.
+
+
+ Il faut donc voir ce calcul comme *le montant qui devra de toute façon être
+ payé* à court terme après 2 ans d'exercice.
+ titre.en: '[automatic] professional income (net taxable)'
+ titre.fr: revenu professionnel (net imposable)
dirigeant . indépendant . revenus étrangers:
description.en: >
[automatic] Foreign income is income declared by self-employed persons in
diff --git a/source/reducers/rootReducer.ts b/source/reducers/rootReducer.ts
index c496e64f6..d781ec1f1 100644
--- a/source/reducers/rootReducer.ts
+++ b/source/reducers/rootReducer.ts
@@ -1,6 +1,6 @@
import { Action } from 'Actions/actions'
import { Analysis } from 'Engine/traverse'
-import { areUnitConvertible, convertUnit, parseUnit, Unit } from 'Engine/units'
+import { Unit } from 'Engine/units'
import { defaultTo, identity, omit, without } from 'ramda'
import reduceReducers from 'reduce-reducers'
import { combineReducers, Reducer } from 'redux'
@@ -8,6 +8,7 @@ import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
import { SavedSimulation } from 'Selectors/storageSelectors'
import { DottedName } from 'Types/rule'
import i18n, { AvailableLangs } from '../i18n'
+import { areUnitConvertible, convertUnit, parseUnit } from './../engine/units'
import inFranceAppReducer, { Company } from './inFranceAppReducer'
import storageRootReducer from './storageReducer'
@@ -37,7 +38,7 @@ type Example = null | {
name: string
situation: object
dottedName: DottedName
- defaultUnits?: Array
+ defaultUnit?: Unit
}
function currentExample(state: Example = null, action: Action): Example {
@@ -155,7 +156,7 @@ export type SimulationConfig = Partial<{
bloquant: Array
situation: Simulation['situation']
branches: Array<{ nom: string; situation: SimulationConfig['situation'] }>
- 'unités par défaut': [string]
+ 'unité par défaut': string
}>
type Situation = Partial>
@@ -165,7 +166,7 @@ export type Simulation = {
hiddenControls: Array
situation: Situation
initialSituation: Situation
- defaultUnits: [string]
+ defaultUnit: string
foldedSteps: Array
unfoldedStep?: DottedName | null
}
@@ -203,7 +204,7 @@ function simulation(
hiddenControls: [],
situation: companySituation,
initialSituation: companySituation,
- defaultUnits: config['unités par défaut'] || ['€/mois'],
+ defaultUnit: config['unité par défaut'] || '€/mois',
foldedSteps: Object.keys(companySituation) as Array,
unfoldedStep: null
}
@@ -251,11 +252,11 @@ function simulation(
case 'UPDATE_DEFAULT_UNIT':
return {
...state,
- defaultUnits: [action.defaultUnit],
situation: updateDefaultUnit(state.situation, {
toUnit: action.defaultUnit,
analysis
- })
+ }),
+ defaultUnit: action.defaultUnit
}
}
return state
diff --git a/source/selectors/analyseSelectors.ts b/source/selectors/analyseSelectors.ts
index e6579d3db..da42e26b0 100644
--- a/source/selectors/analyseSelectors.ts
+++ b/source/selectors/analyseSelectors.ts
@@ -110,8 +110,8 @@ let validatedStepsSelector = createSelector(
[state => state.simulation?.foldedSteps, targetNamesSelector],
(foldedSteps, targetNames) => [...(foldedSteps || []), ...targetNames]
)
-export const defaultUnitsSelector = (state: RootState) =>
- state.simulation?.defaultUnits || []
+export const defaultUnitSelector = (state: RootState) =>
+ state.simulation?.defaultUnit ?? '€/mois'
let branchesSelector = (state: RootState) => configSelector(state).branches
let configSituationSelector = (state: RootState) =>
configSelector(state).situation || {}
@@ -172,9 +172,9 @@ export let ruleAnalysisSelector = createSelector(
(_, props: { dottedName: DottedName }) => props.dottedName,
situationsWithDefaultsSelector,
state => state.situationBranch || 0,
- defaultUnitsSelector
+ defaultUnitSelector
],
- (rules, dottedName, situations, situationBranch, defaultUnits) => {
+ (rules, dottedName, situations, situationBranch, defaultUnit) => {
return analyseRule(
rules,
dottedName,
@@ -184,7 +184,7 @@ export let ruleAnalysisSelector = createSelector(
: situations
return currentSituation[dottedName]
},
- defaultUnits
+ [defaultUnit]
)
}
)
@@ -217,7 +217,7 @@ export let exampleAnalysisSelector = createSelector(
rules,
dottedName,
(dottedName: DottedName) => situation[dottedName],
- example?.defaultUnits
+ example?.defaultUnit
)
)
@@ -227,18 +227,16 @@ let makeAnalysisSelector = (situationSelector: SituationSelectorType) =>
parsedRulesSelector,
targetNamesSelector,
situationSelector,
- defaultUnitsSelector
+ defaultUnitSelector
],
- (parsedRules, targetNames, situations, defaultUnits) => {
+ (parsedRules, targetNames, situations, defaultUnit) => {
return mapOrApply(
situation =>
- analyseMany(
- parsedRules,
- targetNames,
- defaultUnits
- )((dottedName: DottedName) => {
- return situation[dottedName]
- }),
+ analyseMany(parsedRules, targetNames, [defaultUnit])(
+ (dottedName: DottedName) => {
+ return situation[dottedName]
+ }
+ ),
situations
)
}
diff --git a/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx b/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx
index 0790f69d2..e0e69ea19 100644
--- a/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx
+++ b/source/sites/mon-entreprise.fr/pages/Gérer/AideDéclarationIndépendant/Récapitulatif.tsx
@@ -12,7 +12,6 @@ export function AideDéclarationIndépendantsRécapitulatif() {
const siren = useSelector(
(state: RootState) => state.inFranceApp.existingCompany?.siren
)
- console.log(useSelector((state: RootState) => state.rules))
const componentRef = useRef(null)
return (
diff --git a/test/bug-cotisations.test.js b/test/bug-cotisations.test.js
index c7ac3a7c9..72c5963a3 100644
--- a/test/bug-cotisations.test.js
+++ b/test/bug-cotisations.test.js
@@ -55,7 +55,6 @@ describe('bug-analyse-many', function() {
const one = analyse(rules, 'cotisations')(stateSelector).targets[0]
- //console.log(many[0].nodeValue, many[1].nodeValue, one.nodeValue)
expect(many[1].nodeValue).to.be.closeTo(one.nodeValue, 0.1)
})
it('should compute the same contributions if asked with analyseMany or analyse', function() {
diff --git a/test/conversation.test.js b/test/conversation.test.js
index dc64fddcf..2e041e956 100644
--- a/test/conversation.test.js
+++ b/test/conversation.test.js
@@ -8,7 +8,7 @@ import {
nextStepsSelector
} from '../source/selectors/analyseSelectors'
let baseState = {
- simulation: { situation: {}, foldedSteps: [] }
+ simulation: { defaultUnit: '€/an', situation: {}, foldedSteps: [] }
}
describe('conversation', function() {
@@ -24,7 +24,11 @@ describe('conversation', function() {
rules = rawRules.map(enrichRule),
state = merge(baseState, {
rules,
- simulation: { config: { objectifs: ['startHere'] }, foldedSteps: [] }
+ simulation: {
+ defaultUnit: '€/an',
+ config: { objectifs: ['startHere'] },
+ foldedSteps: []
+ }
}),
currentQuestion = currentQuestionSelector(state)
@@ -48,7 +52,11 @@ describe('conversation', function() {
let step1 = merge(baseState, {
rules,
- simulation: { config: { objectifs: ['startHere'] }, foldedSteps: [] }
+ simulation: {
+ defaultUnit: '€/an',
+ config: { objectifs: ['startHere'] },
+ foldedSteps: []
+ }
})
let step2 = reducers(
assocPath(['simulation', 'situation'], { 'top . aa': '1' }, step1),
@@ -125,7 +133,11 @@ describe('conversation', function() {
let step1 = merge(baseState, {
rules,
- simulation: { config: { objectifs: ['net'] }, foldedSteps: [] }
+ simulation: {
+ defaultUnit: '€/an',
+ config: { objectifs: ['net'] },
+ foldedSteps: []
+ }
})
expect(currentQuestionSelector(step1)).to.equal('brut')
@@ -148,7 +160,11 @@ describe('real conversation', function() {
it('should not have more than X questions', function() {
let state = merge(baseState, {
rules,
- simulation: { config: salariéConfig, foldedSteps: [] }
+ simulation: {
+ defaultUnit: '€/an',
+ config: salariéConfig,
+ foldedSteps: []
+ }
}),
nextSteps = nextStepsSelector(state)
diff --git a/test/date.test.js b/test/date.test.js
new file mode 100644
index 000000000..b8c6779ee
--- /dev/null
+++ b/test/date.test.js
@@ -0,0 +1,19 @@
+import { expect } from 'chai'
+import { getDifferenceInMonths } from '../source/engine/date'
+
+describe('Date : getDifferenceInMonths', () => {
+ it('should compute the difference for one full month', () => {
+ expect(getDifferenceInMonths('01/01/2020', '31/01/2020')).to.equal(1)
+ })
+ it('should compute the difference for one month and one day', () => {
+ expect(getDifferenceInMonths('01/01/2020', '01/02/2020')).to.equal(
+ 1 + 1 / 29
+ )
+ })
+ it('should compute the difference for 2 days between months', () => {
+ expect(getDifferenceInMonths('31/01/2020', '01/02/2020')).to.approximately(
+ 1 / 31 + 1 / 29,
+ 0.000000000001
+ )
+ })
+})
diff --git a/test/ficheDePaieSelector.test.js b/test/ficheDePaieSelector.test.js
index ee94e05d5..2081fd3cc 100644
--- a/test/ficheDePaieSelector.test.js
+++ b/test/ficheDePaieSelector.test.js
@@ -10,6 +10,7 @@ import {
let state = {
rules,
simulation: {
+ defaultUnit: '€/mois',
config: salariéConfig,
situation: {
'contrat salarié . rémunération . brut de base': '2300',
diff --git a/test/generateQuestions.test.js b/test/generateQuestions.test.js
index 4053b7b62..0f99293b7 100644
--- a/test/generateQuestions.test.js
+++ b/test/generateQuestions.test.js
@@ -237,8 +237,6 @@ describe('nextSteps', function() {
analysis = analyse(rules, 'sum')(stateSelector),
result = collectMissingVariables(analysis.targets)
- // console.log('analysis', JSON.stringify(analysis, null, 4))
-
expect(result).to.have.lengthOf(1)
expect(result[0]).to.equal('top . sum . evt')
})
diff --git a/test/library.test.js b/test/library.test.js
index 1b480c1df..0a8ff82e4 100644
--- a/test/library.test.js
+++ b/test/library.test.js
@@ -88,13 +88,13 @@ impôt sur le revenu:
assiette: revenu abattu
tranches:
- taux: 0%
- plafond: 9807
+ plafond: 9807 €
- taux: 14%
- plafond: 27086
+ plafond: 27086 €
- taux: 30%
- plafond: 72617
+ plafond: 72617 €
- taux: 41%
- plafond: 153783
+ plafond: 153783 €
- taux: 45%
impôt sur le revenu à payer:
diff --git a/test/mécanismes/grille.yaml b/test/mécanismes/grille.yaml
index 30402c554..c3af15e9b 100644
--- a/test/mécanismes/grille.yaml
+++ b/test/mécanismes/grille.yaml
@@ -31,3 +31,35 @@ Grille:
situation:
assiette: 999.3
valeur attendue: 50
+
+plafond:
+ unité: €
+Grille avec valeur manquante:
+ formule:
+ grille:
+ assiette: assiette
+ unité: €
+ tranches:
+ - montant: 100
+ plafond: plafond
+ - montant: 200
+ plafond: 2000 €
+ - montant: 300
+ plafond: 4000 €
+
+ unité attendue: €
+ exemples:
+ - nom: 'variable manquante'
+ situation:
+ assiette: 1000
+ variables manquantes:
+ - plafond
+ valeur attendue: null
+ - nom: 'assiette non concernée par variable manquante'
+ situation:
+ assiette: 3000
+ valeur attendue: 300
+ - nom: 'assiette au delà du plagond'
+ situation:
+ assiette: 5000
+ valeur attendue: false
diff --git a/test/mécanismes/régularisation.yaml b/test/mécanismes/régularisation.yaml
new file mode 100644
index 000000000..4645c1859
--- /dev/null
+++ b/test/mécanismes/régularisation.yaml
@@ -0,0 +1,93 @@
+salaire:
+ unité: €/mois
+ formule:
+ somme:
+ - 3300 €/mois | du 01/01/2020 | au 29/02/2020
+ - 3600 €/mois | du 01/03/2020 | au 31/12/2020
+
+plafond sécurité sociale:
+ unité: €/mois
+ formule: 3500 €/mois | du 01/01/2020 | au 31/12/2020
+
+retraite:
+ unité: €/mois
+ formule:
+ multiplication:
+ assiette: salaire
+ plafond: plafond sécurité sociale
+ taux: 10%
+
+retraite . avec régularisation:
+ formule:
+ régularisation:
+ règle: retraite
+ valeurs cumulées:
+ - salaire
+ - plafond sécurité sociale
+
+régularisation . avant passage:
+ formule: retraite . avec régularisation | du 01/01/2020 | au 29/02/2020
+ exemples:
+ - valeur attendue: 330
+
+régularisation . test mois régularisés:
+ formule: retraite . avec régularisation | du 01/03/2020 | au 30/06/2020
+ exemples:
+ - valeur attendue: 360
+
+régularisation . test mois après régularisation:
+ formule: retraite . avec régularisation | du 01/07/2020 | au 31/12/2020
+ exemples:
+ - valeur attendue: 350
+
+# ======================
+# Exemple plus complexe
+# ======================
+
+heures d'absences:
+ # TODO : mettre les heures chaque jour
+ formule:
+ somme:
+ - 2 heures/mois | du 01/01/2020 | au 31/01/2020
+ - 3 heures/mois | du 01/03/2020 | au 31/03/2020
+
+temps contractuel:
+ formule: 145 heures/mois
+
+temps de travail effectif:
+ formule: temps contractuel - heures d'absences
+
+plafond sécurité sociale proratisé:
+ formule:
+ multiplication:
+ assiette: plafond sécurité sociale
+ facteur: temps de travail effectif / 151.67 heures/mois
+
+taux variable:
+ formule:
+ variations:
+ - si: salaire < plafond sécurité sociale proratisé
+ alors: 10%
+ - sinon: 20%
+
+cotisation spéciale:
+ unité: €/mois
+ formule:
+ régularisation:
+ règle:
+ multiplication:
+ assiette: salaire
+ taux: taux variable
+ valeurs cumulées:
+ - salaire
+ - plafond sécurité sociale proratisé
+
+régularisation . test variations 1:
+ formule: cotisation spéciale | du 01/01/2020 | au 31/12/2020
+ exemples:
+ - valeur attendue: 710
+
+régularisation . test variations 2:
+ formule: cotisation spéciale | du 01/02/2020 | au 29/02/2020
+ exemples:
+ - valeur attendue: 660
diff --git a/test/mécanismes/variable-temporelle.yaml b/test/mécanismes/variable-temporelle.yaml
new file mode 100644
index 000000000..523f81af8
--- /dev/null
+++ b/test/mécanismes/variable-temporelle.yaml
@@ -0,0 +1,251 @@
+variable temporelle numérique . le . valeur:
+ formule: 40 €/mois | le 02/04/2019
+
+variable temporelle numérique . le . test date applicable:
+ formule: valeur | le 02/04/2019
+ exemples:
+ - valeur attendue: 40
+
+variable temporelle numérique . le . test date non applicable:
+ formule: valeur | le 02/03/2021
+ exemples:
+ - valeur attendue: false
+
+variable temporelle numérique . depuis . valeur:
+ formule: 40 €/mois | depuis le 02/04/2019
+
+variable temporelle numérique . depuis . test date applicable:
+ formule: valeur | depuis le 06/04/2019
+ exemples:
+ - valeur attendue: 40
+
+variable temporelle numérique . depuis . test date non applicable:
+ formule: valeur | le 08/03/2019
+ exemples:
+ - valeur attendue: false
+
+variable temporelle numérique . intervalle . valeur:
+ formule: 40 €/mois | du 02/04/2019 | au 04/05/2020
+
+variable temporelle numérique . intervalle . test date applicable:
+ formule: valeur | le 06/04/2019
+ exemples:
+ - valeur attendue: 40
+
+variable temporelle numérique . intervalle . test date applicable 2:
+ formule: valeur | depuis le 05/06/2019 | jusqu'au 19/04/2020
+ exemples:
+ - valeur attendue: 40
+
+variable temporelle numérique . intervalle . test date non applicable:
+ formule: valeur | le 08/03/2021
+ exemples:
+ - valeur attendue: false
+
+variable temporelle numérique . intervalle . test date non applicable 2:
+ formule: valeur | le 28/01/2019
+ exemples:
+ - valeur attendue: false
+
+variable temporelle numérique . variable . date limite de paiement:
+ formule: 03/09/2020
+
+variable temporelle numérique . variable . majorations de retard:
+ formule: '40 €/jour | à partir de : date limite de paiement'
+
+variable temporelle numérique . variable . test date non applicable:
+ formule: "majorations de retard | jusqu'au : 02/09/2020"
+ exemples:
+ - valeur attendue: false
+
+variable temporelle numérique . variable . test date non applicable 2:
+ formule: majorations de retard | du 01/02/2020 | au 03/08/2020
+ exemples:
+ - valeur attendue: false
+
+variable temporelle numérique . variable . test date applicable:
+ formule: 'majorations de retard | depuis la : date limite de paiement'
+ exemples:
+ - valeur attendue: 40
+
+variable temporelle numérique . variable . test date applicable 2:
+ formule: majorations de retard | le 03/09/2020
+ exemples:
+ - valeur attendue: 40
+
+prix:
+ formule: (20 €/mois | à partir du 15/11/2019) + (10 €/mois | à partir du 01/02/2020)
+
+date:
+variable temporelle numérique . test addition:
+ formule: 'prix | le : date'
+ exemples:
+ - situation:
+ date: 01/01/2019
+ valeur attendue: false
+ - situation:
+ date: 15/12/2019
+ valeur attendue: 20
+ - situation:
+ date: 12/09/2020
+ valeur attendue: 30
+
+prix avec variations:
+ formule: prix * (50% | du 01/01/2020 | au 31/01/2020)
+
+début:
+fin:
+variable temporelle numérique . expression . multiplication:
+ formule: "prix avec variations | depuis : début | jusqu'à : fin"
+ # 20 [avant janvier] / 10 [pendant janvier] | 30 [pendant et après février]
+ exemples:
+ - situation:
+ début: 01/01/2020
+ fin: 31/01/2020
+ valeur attendue: 10
+ - situation:
+ début: 01/01/2020
+ fin: 29/02/2020
+ valeur attendue: 20
+ - situation:
+ début: 01/02/2020
+ fin: 31/03/2020
+ valeur attendue: 30
+
+taux associé:
+ formule:
+ variations:
+ - si: prix avec variations >= 20 €/mois
+ alors: 10%/mois
+ - si: prix avec variations < 20 €/mois
+ alors: 60%/mois
+ # Cette formule peut paraître bizarre, mais lorsque multiplication est non
+ # applicable, c'est bien le sinon qui s'applique
+ - sinon: 5%/mois
+variable temporelle numérique . variation:
+ formule: "taux associé | depuis : début | jusqu'à : fin"
+ exemples:
+ - situation:
+ début: 01/01/2020
+ fin: 31/01/2020
+ valeur attendue: 60
+ - situation:
+ début: 01/01/2020
+ fin: 29/02/2020
+ valeur attendue: 35
+ - situation:
+ début: 01/02/2020
+ fin: 31/03/2020
+ valeur attendue: 10
+ - situation:
+ début: 01/10/2019
+ fin: 30/10/2019
+ valeur attendue: 5
+
+contrat salarié . date d'embauche:
+ formule: 12/09/2018
+
+contrat salarié . salaire:
+ formule:
+ somme:
+ - brut de base
+ - primes
+
+contrat salarié . salaire . brut de base:
+ formule:
+ somme:
+ - "2000€/mois | depuis : date d'embauche | jusqu'au 08/08/2019"
+ - 2200€/mois | depuis le 09/08/2019
+
+contrat salarié . salaire . primes:
+ formule: 2000€/mois | du 01/12/2019 | au 31/12/2019
+
+plafond sécurité sociale:
+ formule:
+ somme:
+ - 3377 €/mois | du 01/01/2019 | au 31/12/2019
+ - 3424 €/mois | du 01/01/2020 | au 31/12/2020
+
+contrat salarié . cotisations . retraite:
+ formule:
+ multiplication:
+ assiette: salaire
+ plafond: plafond sécurité sociale
+ taux: 10%
+
+variable temporelle numérique . somme:
+ formule: contrat salarié . salaire | du 01/12/2019 | au 31/12/2019
+ exemples:
+ - valeur attendue: 4200 # 2000 + 2200
+
+variable temporelle numérique . somme avec valeur changeant au cours du mois:
+ formule: contrat salarié . salaire | du 01/08/2019 | au 31/08/2019
+ exemples:
+ - valeur attendue: 2148.387 # (2000 * 8 + 2200 * 23)/31
+
+variable temporelle numérique . multiplication:
+ formule: contrat salarié . cotisations . retraite | du 01/05/2019 | au 31/05/2019
+ exemples:
+ - valeur attendue: 200 # 2000 * 10%
+
+variable temporelle numérique . multiplication avec valeur changeant au cours du mois:
+ formule: contrat salarié . cotisations . retraite | du 01/08/2019 | au 31/08/2019
+ exemples:
+ - valeur attendue: 214.839 # (2000 * 8 + 2200 * 23)/31
+
+variable temporelle numérique . multiplication avec valeur au dessus du plafond:
+ formule: contrat salarié . cotisations . retraite | du 01/12/2019 | au 31/12/2019
+ exemples:
+ - valeur attendue: 337.7 # (2000 * 8 + 2200 * 23)/31
+
+variable temporelle numérique . multiplication avec valeur sur l'année:
+ formule: contrat salarié . cotisations . retraite | du 01/01/2019 | au 31/12/2019
+ exemples:
+ # 200 * 7 [janvier-juin]
+ # + 214.839 [juillet]
+ # + 220 * 3 [aout-novembre]
+ # + 337.7 [décembre]
+ # /12 mois
+ - valeur attendue: 217.7115
+# test . proratisation du salaire avec entrée en cours de mois:
+# formule: salaire brut [avril 2019]
+# exemples:
+# - valeur attendue: 400 # (2000 * 6)/30
+cotisation spéciale:
+ formule:
+ barème:
+ assiette: contrat salarié . salaire
+ multiplicateur: plafond sécurité sociale
+ tranches:
+ - taux: 0%
+ plafond: 10%
+ - taux: 10%
+ plafond: 20%
+ - taux: 30%
+ plafond: 50%
+ - taux: 40%
+ plafond: 100%
+ - taux: 50%
+
+variable temporelle numérique . barème:
+ formule: cotisation spéciale | du 01/01/2019 | au 31/12/2019
+ exemples:
+ - valeur attendue: 567.438
+
+grille:
+ formule:
+ barème:
+ assiette: contrat salarié . salaire
+ tranches:
+ - montant: 5 heures
+ plafond: 1000€
+ - montant: 10 heures
+ plafond: 2000 €
+ - montant: 30 heures
+ plafond: 4000 €
+ - montant: 40 heures
+
+variable temporelle numérique . grille:
+ formule: cotisation spéciale | du 01/01/2019 | au 31/12/2019
+ exemples:
+ - valeur attendue: 567.438
diff --git a/test/period.test.js b/test/period.test.js
new file mode 100644
index 000000000..4316b3869
--- /dev/null
+++ b/test/period.test.js
@@ -0,0 +1,132 @@
+import { expect } from 'chai'
+import {
+ concatTemporals,
+ createTemporalEvaluation,
+ groupByYear,
+ zipTemporals
+} from '../source/engine/temporal'
+
+const neverEnding = value => [{ start: null, end: null, value: value }]
+describe('Periods : zip', () => {
+ it('should zip two empty temporalValue', () => {
+ const result = zipTemporals([], [])
+ expect(result).to.deep.equal([])
+ })
+
+ it('should zip constant temporalValue', () => {
+ const result = zipTemporals(neverEnding(1), neverEnding(2))
+ expect(result).to.deep.equal(neverEnding([1, 2]))
+ })
+
+ it('should zip changing temporalValue', () => {
+ const value1 = createTemporalEvaluation(true, {
+ start: null,
+ end: '01/08/2020'
+ })
+ const value2 = neverEnding(1)
+ expect(zipTemporals(value1, value2)).to.deep.equal([
+ { start: null, end: '01/08/2020', value: [true, 1] },
+ { start: '02/08/2020', end: null, value: [false, 1] }
+ ])
+ expect(zipTemporals(value2, value1)).to.deep.equal([
+ { start: null, end: '01/08/2020', value: [1, true] },
+ { start: '02/08/2020', end: null, value: [1, false] }
+ ])
+ })
+
+ it('should zip two overlapping temporalValue', () => {
+ const value1 = createTemporalEvaluation(1, {
+ start: '01/07/2019',
+ end: '30/06/2020'
+ })
+ const value2 = createTemporalEvaluation(2, {
+ start: '01/01/2019',
+ end: '31/12/2019'
+ })
+
+ expect(zipTemporals(value1, value2)).to.deep.equal([
+ { start: null, end: '31/12/2018', value: [false, false] },
+ { start: '01/01/2019', end: '30/06/2019', value: [false, 2] },
+ { start: '01/07/2019', end: '31/12/2019', value: [1, 2] },
+ { start: '01/01/2020', end: '30/06/2020', value: [1, false] },
+ { start: '01/07/2020', end: null, value: [false, false] }
+ ])
+ })
+})
+
+describe('Periods : concat', () => {
+ it('should merge concat overlapping temporalValue', () => {
+ const value1 = createTemporalEvaluation(10)
+ const value2 = [
+ { start: null, end: '14/04/2019', value: 100 },
+ { start: '15/04/2019', end: '08/08/2019', value: 2000 },
+ { start: '09/08/2019', end: null, value: 200 }
+ ]
+
+ expect(concatTemporals([value1, value2])).to.deep.equal([
+ { start: null, end: '14/04/2019', value: [10, 100] },
+ { start: '15/04/2019', end: '08/08/2019', value: [10, 2000] },
+ { start: '09/08/2019', end: null, value: [10, 200] }
+ ])
+ })
+})
+
+describe('Periods : groupByYear', () => {
+ const invariants = temporalYear => {
+ const startDate = temporalYear[0].start
+ const endDate = temporalYear.slice(-1)[0].end
+ expect(
+ startDate === null || startDate.startsWith('01/01'),
+ 'starts at the beginning of a year'
+ )
+ expect(
+ endDate === null || endDate.startsWith('31/12'),
+ 'stops at the end of a year'
+ )
+ }
+ it('should handle constant value', () => {
+ const value = createTemporalEvaluation(10)
+ expect(groupByYear(value)).to.deep.equal([value])
+ })
+ it('should handle changing value', () => {
+ const value = createTemporalEvaluation(10, {
+ start: '06/06/2020',
+ end: '20/12/2020'
+ })
+ const result = groupByYear(value)
+ expect(result).to.have.length(3)
+ result.forEach(invariants)
+ })
+ it('should handle changing value over several years', () => {
+ const value = createTemporalEvaluation(10, {
+ start: '06/06/2020',
+ end: '20/12/2022'
+ })
+ const result = groupByYear(value)
+ expect(result).to.have.length(5)
+ result.forEach(invariants)
+ })
+ it('should handle complex case', () => {
+ const result = groupByYear(
+ concatTemporals([
+ createTemporalEvaluation(1, {
+ start: '06/06/2020',
+ end: '20/12/2022'
+ }),
+ createTemporalEvaluation(2, {
+ start: '01/01/1991',
+ end: '20/12/1992'
+ }),
+ createTemporalEvaluation(3, {
+ start: '31/01/1990',
+ end: '20/12/2021'
+ }),
+ createTemporalEvaluation(4, {
+ start: '31/12/2020',
+ end: '01/01/2021'
+ })
+ ])
+ )
+ result.forEach(invariants)
+ })
+})
diff --git a/test/rules/sasu.yaml b/test/rules/sasu.yaml
index 1afe5ab0f..c4c345a88 100644
--- a/test/rules/sasu.yaml
+++ b/test/rules/sasu.yaml
@@ -4,9 +4,11 @@
chiffre affaires:
unité par défaut: €/mois
+ par défaut: 0
charges:
- par défaut: 0 €/mois
+ unité: €/mois
+ par défaut: 0
répartition salaire sur dividendes:
par défaut: 50
diff --git a/test/temporal.test.js b/test/temporal.test.js
new file mode 100644
index 000000000..4316b3869
--- /dev/null
+++ b/test/temporal.test.js
@@ -0,0 +1,132 @@
+import { expect } from 'chai'
+import {
+ concatTemporals,
+ createTemporalEvaluation,
+ groupByYear,
+ zipTemporals
+} from '../source/engine/temporal'
+
+const neverEnding = value => [{ start: null, end: null, value: value }]
+describe('Periods : zip', () => {
+ it('should zip two empty temporalValue', () => {
+ const result = zipTemporals([], [])
+ expect(result).to.deep.equal([])
+ })
+
+ it('should zip constant temporalValue', () => {
+ const result = zipTemporals(neverEnding(1), neverEnding(2))
+ expect(result).to.deep.equal(neverEnding([1, 2]))
+ })
+
+ it('should zip changing temporalValue', () => {
+ const value1 = createTemporalEvaluation(true, {
+ start: null,
+ end: '01/08/2020'
+ })
+ const value2 = neverEnding(1)
+ expect(zipTemporals(value1, value2)).to.deep.equal([
+ { start: null, end: '01/08/2020', value: [true, 1] },
+ { start: '02/08/2020', end: null, value: [false, 1] }
+ ])
+ expect(zipTemporals(value2, value1)).to.deep.equal([
+ { start: null, end: '01/08/2020', value: [1, true] },
+ { start: '02/08/2020', end: null, value: [1, false] }
+ ])
+ })
+
+ it('should zip two overlapping temporalValue', () => {
+ const value1 = createTemporalEvaluation(1, {
+ start: '01/07/2019',
+ end: '30/06/2020'
+ })
+ const value2 = createTemporalEvaluation(2, {
+ start: '01/01/2019',
+ end: '31/12/2019'
+ })
+
+ expect(zipTemporals(value1, value2)).to.deep.equal([
+ { start: null, end: '31/12/2018', value: [false, false] },
+ { start: '01/01/2019', end: '30/06/2019', value: [false, 2] },
+ { start: '01/07/2019', end: '31/12/2019', value: [1, 2] },
+ { start: '01/01/2020', end: '30/06/2020', value: [1, false] },
+ { start: '01/07/2020', end: null, value: [false, false] }
+ ])
+ })
+})
+
+describe('Periods : concat', () => {
+ it('should merge concat overlapping temporalValue', () => {
+ const value1 = createTemporalEvaluation(10)
+ const value2 = [
+ { start: null, end: '14/04/2019', value: 100 },
+ { start: '15/04/2019', end: '08/08/2019', value: 2000 },
+ { start: '09/08/2019', end: null, value: 200 }
+ ]
+
+ expect(concatTemporals([value1, value2])).to.deep.equal([
+ { start: null, end: '14/04/2019', value: [10, 100] },
+ { start: '15/04/2019', end: '08/08/2019', value: [10, 2000] },
+ { start: '09/08/2019', end: null, value: [10, 200] }
+ ])
+ })
+})
+
+describe('Periods : groupByYear', () => {
+ const invariants = temporalYear => {
+ const startDate = temporalYear[0].start
+ const endDate = temporalYear.slice(-1)[0].end
+ expect(
+ startDate === null || startDate.startsWith('01/01'),
+ 'starts at the beginning of a year'
+ )
+ expect(
+ endDate === null || endDate.startsWith('31/12'),
+ 'stops at the end of a year'
+ )
+ }
+ it('should handle constant value', () => {
+ const value = createTemporalEvaluation(10)
+ expect(groupByYear(value)).to.deep.equal([value])
+ })
+ it('should handle changing value', () => {
+ const value = createTemporalEvaluation(10, {
+ start: '06/06/2020',
+ end: '20/12/2020'
+ })
+ const result = groupByYear(value)
+ expect(result).to.have.length(3)
+ result.forEach(invariants)
+ })
+ it('should handle changing value over several years', () => {
+ const value = createTemporalEvaluation(10, {
+ start: '06/06/2020',
+ end: '20/12/2022'
+ })
+ const result = groupByYear(value)
+ expect(result).to.have.length(5)
+ result.forEach(invariants)
+ })
+ it('should handle complex case', () => {
+ const result = groupByYear(
+ concatTemporals([
+ createTemporalEvaluation(1, {
+ start: '06/06/2020',
+ end: '20/12/2022'
+ }),
+ createTemporalEvaluation(2, {
+ start: '01/01/1991',
+ end: '20/12/1992'
+ }),
+ createTemporalEvaluation(3, {
+ start: '31/01/1990',
+ end: '20/12/2021'
+ }),
+ createTemporalEvaluation(4, {
+ start: '31/12/2020',
+ end: '01/01/2021'
+ })
+ ])
+ )
+ result.forEach(invariants)
+ })
+})