Merge pull request #866 from betagouv/engine-next

RFC : Gestion des variables temporelles et de la proratisation
pull/932/head
Johan Girod 2020-03-18 16:30:29 +01:00 committed by GitHub
commit 140913f973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 2263 additions and 587 deletions

View File

@ -1,5 +1,6 @@
const fr = Cypress.env('language') === 'fr'
const inputSelector = 'input.currencyInput__input:not([name$="charges"])'
const chargeInputSelector = 'input.currencyInput__input[name$="charges"]'
describe('Simulateurs', function() {
if (!fr) {
return
@ -15,7 +16,7 @@ describe('Simulateurs', function() {
it('should display a result when entering a value in any of the currency input', () => {
cy.contains('€ / an').click()
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
cy.get('input.currencyInput__input[name$="charges"]').type(1000)
cy.get(chargeInputSelector).type(1000)
}
cy.get(inputSelector).each((testedInput, i) => {
cy.wrap(testedInput).type('{selectall}60000')
@ -37,12 +38,21 @@ describe('Simulateurs', function() {
cy.get(inputSelector)
.first()
.type('{selectall}12000')
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
cy.get(chargeInputSelector).type('{selectall}6000')
}
cy.wait(600)
cy.contains('€ / mois').click()
cy.get(inputSelector)
.first()
.invoke('val')
.should('match', /1[\s]000/)
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
cy.get(chargeInputSelector)
.first()
.invoke('val')
.should('be', '500')
}
})
it('should allow to navigate to a documentation page', function() {

View File

@ -426,6 +426,15 @@ dirigeant . indépendant . revenu net de cotisations:
dirigeant . indépendant . revenu professionnel:
unité par défaut: €/an
titre: revenu professionnel (net imposable)
description: |
C'est le revenu net de cotisations déductibles du travailleur indépendant, qui sert de base au calcul des cotisations et de l'impôt pour les indépendants.
Attention, **notre calcul est fait au régime de croisière**:
l'indépendant qui se lance paiera pendant ses 2 premières années un forfait relativement réduit de cotisations sociales. Il devra ensuite régulariser cette situation par rapport au revenu qu'il a vraiment perçu.
Il faut donc voir ce calcul comme *le montant qui devra de toute façon être payé* à court terme après 2 ans d'exercice.
formule:
inversion numérique:
avec:
@ -437,7 +446,6 @@ dirigeant . indépendant . revenu professionnel:
valeurs négatives possibles: oui
dirigeant . indépendant . assiette des cotisations:
unité par défaut: €/an
formule:
encadrement:
plancher: 0
@ -1032,7 +1040,6 @@ dirigeant . indépendant . cotisations et contributions . cotisations . allocati
taux: 3.1%
dirigeant . indépendant . cotisations et contributions . exonérations:
période: flexible
formule:
somme:
- ZFU

View File

@ -12,7 +12,6 @@ impôt:
produit:
assiette: revenu imposable
taux: taux du prélèvement à la source
- sinon: 0
- CEHR
- dirigeant . auto-entrepreneur . impôt . versement libératoire . montant
@ -358,6 +357,7 @@ impôt . taux personnalisé:
revenus net de cotisations:
résumé: Avant impôt
unité: €/an
question: Quel revenu avant impôt voulez-vous toucher ?
description: |
Il s'agit du revenu net de cotisations et de charges, avant le paiement de l'impôt sur le revenu.

View File

@ -1876,11 +1876,6 @@ contrat salarié . statut JEI . exonération de cotisations:
- vieillesse .employeur
contrat salarié . réduction générale:
aide:
type: réduction de cotisations
thème: aide bas salaires
démarches: non
alias: réduction fillon
description: |
Dans le cadre du pacte de responsabilité et de solidarité, le dispositif zéro cotisation Urssaf permet à l'employeur d'un salarié au Smic de ne plus payer aucune cotisation. Le montant de l'allègement est égal au produit de la rémunération annuelle brute par un coefficient. Il n'y a pas de formalité particulière à effectuer.
références:
@ -2903,13 +2898,12 @@ contrat salarié . taxe sur les salaires:
entreprise . effectif: 1
valeur attendue: 0
- nom: association non lucrative
unités par défaut: [€/mois]
situation:
entreprise . association non lucrative: oui
rémunération . brut de base: 2300
entreprise . effectif: 10
complémentaire santé . forfait: 0
valeur attendue: 48.10
valeur attendue: 577
références:
fiche: https://www.service-public.fr/professionnels-entreprises/vosdroits/F22576

View File

@ -103,13 +103,13 @@ export default function PaySlip() {
<Value
nilValueSymbol="—"
{...getRule('contrat salarié . cotisations . patronales')}
unit="€"
printedUnit="€"
className="payslip__total"
/>
<Value
nilValueSymbol="—"
{...getRule('contrat salarié . cotisations . salariales')}
unit="€"
printedUnit="€"
className="payslip__total"
/>
{/* Salaire chargé */}

View File

@ -1,6 +1,8 @@
import Value from 'Components/Value'
import React from 'react'
import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux'
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
import RuleLink from './RuleLink'
export let SalaireBrutSection = ({ getRule }) => {
@ -43,12 +45,20 @@ export let SalaireBrutSection = ({ getRule }) => {
)
}
export let Line = ({ rule, ...props }) => (
<>
<RuleLink {...rule} />
<Value {...rule} nilValueSymbol="—" unit="€" {...props} />
</>
)
export let Line = ({ rule, ...props }) => {
const defaultUnit = useSelector(defaultUnitSelector)
return (
<>
<RuleLink {...rule} />
<Value
{...rule}
nilValueSymbol="—"
defaultUnit={defaultUnit}
{...props}
/>
</>
)
}
export let SalaireNetSection = ({ getRule }) => {
let avantagesEnNature = getRule(

View File

@ -3,13 +3,14 @@ import { parseUnit, serializeUnit } from 'Engine/units'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { defaultUnitsSelector } from 'Selectors/analyseSelectors'
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
import './PeriodSwitch.css'
export default function PeriodSwitch() {
const dispatch = useDispatch()
const currentUnit = useSelector(defaultUnitsSelector)[0]
const language = useTranslation().i18n.language
const currentUnit = useSelector(defaultUnitSelector)
let units = ['€/mois', '€/an']
return (
<span id="PeriodSwitch">

View File

@ -10,7 +10,7 @@ import { useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
defaultUnitsSelector
defaultUnitSelector
} from 'Selectors/analyseSelectors'
import * as Animate from 'Ui/animate'
@ -51,9 +51,9 @@ export default function SalaryExplanation() {
{emoji('📊')} <Trans>Voir la répartition des cotisations</Trans>
</button>
</div>
<PaySlipSection />
<div ref={distributionRef}>
<DistributionSection />
<PaySlipSection />
</div>
</>
)}
@ -117,7 +117,7 @@ function RevenueRepatitionSection() {
}
function PaySlipSection() {
const unit = useSelector(defaultUnitsSelector)[0]
const unit = useSelector(defaultUnitSelector)
return (
<section>
<h2>

View File

@ -26,12 +26,16 @@ export type ValueProps = Partial<
children: number
negative: boolean
customCSS: string
defaultUnit?: string
printedUnit?: string
}
>
export default function Value({
nodeValue: value,
unit,
defaultUnit,
printedUnit,
nilValueSymbol,
maximumFractionDigits,
minimumFractionDigits,

View File

@ -2,7 +2,7 @@ import { toPairs } from 'ramda'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { defaultUnitsSelector } from 'Selectors/analyseSelectors'
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
import { convertUnit, parseUnit, Unit } from '../../engine/units'
type InputSuggestionsProps = {
@ -20,7 +20,7 @@ export default function InputSuggestions({
}: InputSuggestionsProps) {
const [suggestion, setSuggestion] = useState<number>()
const { t } = useTranslation()
const defaultUnit = parseUnit(useSelector(defaultUnitsSelector)[0] ?? '')
const defaultUnit = parseUnit(useSelector(defaultUnitSelector) ?? '')
if (!suggestions) return null
return (

View File

@ -1,5 +1,5 @@
situation:
dirigeant: artiste-auteur
unités par défaut: [€/an]
unité par défaut: €/an
objectifs:
- artiste-auteur . cotisations

View File

@ -34,7 +34,7 @@ questions:
- contrat salarié . complémentaire santé . part employeur
- contrat salarié . régime des impatriés
unités par défaut: [€/an]
unité par défaut: €/an
situation:
dirigeant: 'assimilé salarié'
contrat salarié . ATMP . taux réduit: oui

View File

@ -16,6 +16,6 @@ questions:
liste noire:
- entreprise . charges
unités par défaut: [€/an]
unité par défaut: €/an
situation:
dirigeant: 'auto-entrepreneur'

View File

@ -29,6 +29,6 @@ questions:
- dirigeant . indépendant . cotisations et contributions . exonérations . âge
- dirigeant . indépendant . cotisations et contributions . exonérations . invalidité
unités par défaut: [€/an]
unité par défaut: €/an
situation:
dirigeant: 'indépendant'

View File

@ -18,7 +18,7 @@ questions:
- entreprise . catégorie d'activité . restauration ou hébergement
- entreprise . catégorie d'activité . libérale règlementée
unités par défaut: [€/an]
unité par défaut: €/an
branches:
- nom: Assimilé salarié
situation:

View File

@ -27,6 +27,6 @@ questions:
- contrat salarié . statut JEI
- contrat salarié . complémentaire santé . part employeur
- contrat salarié . régime des impatriés
unités par défaut: [€/mois]
unité par défaut: €/mois
situation:
dirigeant: non

View File

@ -25,7 +25,7 @@ export default function AnimatedTargetValue({
// We don't want to show the animated if the difference comes from a change in the unit
const currentUnit = useSelector(
(state: RootState) => state?.simulation?.defaultUnits[0]
(state: RootState) => state?.simulation?.defaultUnit
)
const previousUnit = useRef(currentUnit)

View File

@ -18,22 +18,53 @@ export function normalizeDate(
return `${pad(day)}/${pad(month)}/${pad(year)}`
}
const dateRegexp = /[\d]{2}\/[\d]{2}\/[\d]{4}/
export function convertToDate(value: string): Date {
const [day, month, year] = normalizeDateString(value).split('/')
return new Date(+year, +month - 1, +day)
var result = new Date(+year, +month - 1, +day)
// Reset date to utc midnight for exact calculation of day difference (no
// daylight saving effect)
result.setMinutes(result.getMinutes() - result.getTimezoneOffset())
return result
}
export function convertToDateIfNeeded(...values: string[]) {
const dateStrings = values.map(dateString => '' + dateString)
if (!dateStrings.some(dateString => dateString.match(dateRegexp))) {
return values
}
dateStrings.forEach(dateString => {
if (!dateString.match(dateRegexp)) {
throw new TypeError(
`'${dateString}' n'est pas une date valide (format attendu: mm/aaaa ou jj/mm/aaaa)`
)
}
})
return dateStrings.map(convertToDate)
export function convertToString(date: Date): string {
return normalizeDate(date.getFullYear(), date.getMonth() + 1, date.getDate())
}
export function getRelativeDate(date: string, dayDifferential: number): string {
const relativeDate = new Date(convertToDate(date))
relativeDate.setDate(relativeDate.getDate() + dayDifferential)
return convertToString(relativeDate)
}
export function getYear(date: string): number {
return +date.slice(-4)
}
export function getDifferenceInDays(from: string, to: string): number {
const millisecondsPerDay = 1000 * 60 * 60 * 24
return (
1 +
(convertToDate(from).getTime() - convertToDate(to).getTime()) /
millisecondsPerDay
)
}
export function getDifferenceInMonths(from: string, to: string): number {
// We want to compute the difference in actual month between the two dates
// For date that start during a month, a pro-rata will be done depending on
// the duration of the month in days
const [dayFrom, monthFrom, yearFrom] = from.split('/').map(x => +x)
const [dayTo, monthTo, yearTo] = to.split('/').map(x => +x)
const numberOfFullMonth = monthTo - monthFrom + 12 * (yearTo - yearFrom)
const numDayMonthFrom = new Date(yearFrom, monthFrom, 0).getDate()
const numDayMonthTo = new Date(yearTo, monthTo, 0).getDate()
const prorataMonthFrom = (dayFrom - 1) / numDayMonthFrom
const prorataMonthTo = dayTo / numDayMonthTo
return numberOfFullMonth - prorataMonthFrom + prorataMonthTo
}
export function getDifferenceInYears(from: string, to: string): number {
// Todo : take leap year into account
return getDifferenceInDays(from, to) / 365.25
}

View File

@ -1,14 +1,16 @@
import { coerceArray } from '../utils'
export class EngineError extends Error {}
export function syntaxError(
rules: string[] | string,
message: string,
originalError?: Error
) {
throw new Error(
throw new EngineError(
`\n[ Erreur syntaxique ]
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
${message}
${originalError && originalError.message}
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
${message}
${originalError ? originalError.message : ''}
`
)
}
@ -18,11 +20,11 @@ export function evaluationError(
message: string,
originalError?: Error
) {
throw new Error(
throw new EngineError(
`\n[ Erreur d'évaluation ]
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
${message}
${originalError && originalError.message}
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
${message}
${originalError ? originalError.message : ''}
`
)
}
@ -34,9 +36,9 @@ export function typeWarning(
) {
console.warn(
`\n[ Erreur de type ]
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
${message}
${originalError && originalError.message}
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
${message}
${originalError ? originalError.message : ''}
`
)
}
@ -48,9 +50,9 @@ export function warning(
) {
console.warn(
`\n[ Avertissement ]
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
${message}
💡${solution}
Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
${message}
💡 ${solution}
`
)
}

View File

@ -102,6 +102,7 @@ export default (cache, situationGate, parsedRules, node) => {
node.defaultUnit ||
evaluatedFormula.unit
const temporalValue = evaluatedFormula.temporalValue
if (unit) {
try {
nodeValue = convertNodeToUnit(unit, evaluatedFormula).nodeValue
@ -121,6 +122,7 @@ export default (cache, situationGate, parsedRules, node) => {
...(node.formule && { formule: evaluatedFormula }),
nodeValue,
unit,
temporalValue,
isApplicable,
missingVariables
}

View File

@ -1,170 +0,0 @@
import {
add,
any,
equals,
evolve,
filter,
fromPairs,
is,
keys,
map,
mergeWith,
reduce,
values
} from 'ramda'
import React from 'react'
import { typeWarning } from './error'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
export let makeJsx = node =>
typeof node.jsx == 'function'
? node.jsx(node.nodeValue, node.explanation, node.lazyEval, node.unit)
: node.jsx
export let collectNodeMissing = node => node.missingVariables || {}
export let bonus = (missings, hasCondition = true) =>
hasCondition ? map(x => x + 0.0001, missings || {}) : missings
export let mergeAllMissing = missings =>
reduce(mergeWith(add), {}, map(collectNodeMissing, missings))
export let mergeMissing = (left, right) =>
mergeWith(add, left || {}, right || {})
export let evaluateNode = (cache, situationGate, parsedRules, node) => {
let evaluatedNode = node.evaluate
? node.evaluate(cache, situationGate, parsedRules, node)
: node
if (typeof evaluatedNode.nodeValue !== 'number') {
return evaluatedNode
}
evaluatedNode = node.unité
? convertNodeToUnit(node.unit, evaluatedNode)
: simplifyNodeUnit(evaluatedNode)
return evaluatedNode
}
const sameUnitValues = (explanation, contextRule, mecanismName) => {
const firstNodeWithUnit = explanation.find(node => !!node.unit)
if (!firstNodeWithUnit) {
return [undefined, explanation.map(({ nodeValue }) => nodeValue)]
}
const values = explanation.map(node => {
try {
return convertNodeToUnit(firstNodeWithUnit?.unit, node).nodeValue
} catch (e) {
typeWarning(
contextRule,
`Dans le mécanisme ${mecanismName}, les unités des éléments suivants sont incompatibles entre elles : \n\t\t${node?.name ||
node?.rawNode}\n\t\t${firstNodeWithUnit?.name ||
firstNodeWithUnit?.rawNode}'`,
e
)
return node.nodeValue
}
})
return [firstNodeWithUnit.unit, values]
}
export let evaluateArray = (reducer, start) => (
cache,
situationGate,
parsedRules,
node
) => {
let evaluateOne = child =>
evaluateNode(cache, situationGate, parsedRules, child),
explanation = map(evaluateOne, node.explanation),
[unit, values] = sameUnitValues(
explanation,
cache._meta.contextRule,
node.name
),
nodeValue = values.some(value => value === null)
? null
: reduce(reducer, start, values),
missingVariables =
node.nodeValue == null ? mergeAllMissing(explanation) : {}
return {
...node,
nodeValue,
explanation,
missingVariables,
unit
}
}
export let evaluateArrayWithFilter = (evaluationFilter, reducer, start) => (
cache,
situationGate,
parsedRules,
node
) => {
let evaluateOne = child =>
evaluateNode(cache, situationGate, parsedRules, child),
explanation = map(
evaluateOne,
filter(evaluationFilter(situationGate), node.explanation)
),
[unit, values] = sameUnitValues(
explanation,
cache._meta.contextRule,
node.name
),
nodeValue = any(equals(null), values)
? null
: reduce(reducer, start, values),
missingVariables =
node.nodeValue == null ? mergeAllMissing(explanation) : {}
return { ...node, nodeValue, explanation, missingVariables, unit }
}
export let defaultNode = nodeValue => ({
nodeValue,
// eslint-disable-next-line
jsx: nodeValue => <span className="value">{nodeValue}</span>,
isDefault: true
})
export let parseObject = (recurse, objectShape, value) => {
let recurseOne = key => defaultValue => {
if (value[key] == null && !defaultValue)
throw new Error(
`Il manque une clé '${key}' dans ${JSON.stringify(value)} `
)
return value[key] != null ? recurse(value[key]) : defaultValue
}
let transforms = fromPairs(map(k => [k, recurseOne(k)], keys(objectShape)))
return evolve(transforms, objectShape)
}
export let evaluateObject = (objectShape, effect) => (
cache,
situationGate,
parsedRules,
node
) => {
let evaluateOne = child =>
evaluateNode(cache, situationGate, parsedRules, child)
let transforms = map(k => [k, evaluateOne], keys(objectShape)),
automaticExplanation = evolve(fromPairs(transforms))(node.explanation)
// the result of effect can either be just a nodeValue, or an object {additionalExplanation, nodeValue}. The latter is useful for a richer JSX visualisation of the mecanism : the view should not duplicate code to recompute intermediate values (e.g. for a marginal 'barème', the marginal 'tranche')
let evaluated = effect(
automaticExplanation,
cache,
situationGate,
parsedRules
),
explanation = is(Object, evaluated)
? { ...automaticExplanation, ...evaluated.additionalExplanation }
: automaticExplanation,
nodeValue = is(Object, evaluated) ? evaluated.nodeValue : evaluated,
missingVariables = mergeAllMissing(values(explanation))
return simplifyNodeUnit({
...node,
nodeValue,
explanation,
missingVariables,
unit: explanation.unit
})
}

View File

@ -0,0 +1,212 @@
import {
add,
evolve,
filter,
fromPairs,
keys,
map,
mergeWith,
reduce
} from 'ramda'
import React from 'react'
import { typeWarning } from './error'
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
import {
concatTemporals,
EvaluatedNode,
liftTemporalNode,
mapTemporal,
pureTemporal,
Temporal,
temporalAverage,
zipTemporals
} from './temporal'
export let makeJsx = node =>
typeof node.jsx == 'function'
? node.jsx(node.nodeValue, node.explanation, node.lazyEval, node.unit)
: node.jsx
export let collectNodeMissing = node => node.missingVariables || {}
export let bonus = (missings, hasCondition = true) =>
hasCondition ? map(x => x + 0.0001, missings || {}) : missings
export let mergeAllMissing = missings =>
reduce(mergeWith(add), {}, map(collectNodeMissing, missings))
export let mergeMissing = (left, right) =>
mergeWith(add, left || {}, right || {})
export let evaluateNode = (cache, situationGate, parsedRules, node) => {
let evaluatedNode = node.evaluate
? node.evaluate(cache, situationGate, parsedRules, node)
: node
if (typeof evaluatedNode.nodeValue !== 'number') {
return evaluatedNode
}
evaluatedNode = node.unité
? convertNodeToUnit(node.unit, evaluatedNode)
: simplifyNodeUnit(evaluatedNode)
return evaluatedNode
}
function convertNodesToSameUnit(nodes, contextRule, mecanismName) {
const firstNodeWithUnit = nodes.find(node => !!node.unit)
if (!firstNodeWithUnit) {
return nodes
}
return nodes.map(node => {
try {
return convertNodeToUnit(firstNodeWithUnit.unit, node)
} catch (e) {
typeWarning(
contextRule,
`Dans le mécanisme ${mecanismName}, les unités des éléments suivants sont incompatibles entre elles : \n\t\t${node?.name ||
node?.rawNode}\n\t\t${firstNodeWithUnit?.name ||
firstNodeWithUnit?.rawNode}'`,
e
)
return node
}
})
}
export const evaluateArray = (reducer, start) => (
cache,
situationGate,
parsedRules,
node
) => {
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
const evaluatedNodes = convertNodesToSameUnit(
node.explanation.map(evaluate),
cache._meta.contextRule,
node.name
)
const temporalValues = concatTemporals(
evaluatedNodes.map(
({ temporalValue, nodeValue }) => temporalValue ?? pureTemporal(nodeValue)
)
)
const temporalValue = mapTemporal(values => {
if (values.some(value => value === null)) {
return null
}
return reduce(reducer, start, values)
}, temporalValues)
const baseEvaluation = {
...node,
missingVariables: mergeAllMissing(evaluatedNodes),
explanation: evaluatedNodes,
unit: evaluatedNodes[0].unit
}
if (temporalValue.length === 1) {
return {
...baseEvaluation,
nodeValue: temporalValue[0].value
}
}
return {
...baseEvaluation,
temporalValue,
nodeValue: temporalAverage(temporalValue)
}
}
export const evaluateArrayWithFilter = (evaluationFilter, reducer, start) => (
cache,
situationGate,
parsedRules,
node
) => {
return evaluateArray(reducer, start)(cache, situationGate, parsedRules, {
...node,
explanation: filter(evaluationFilter(situationGate), node.explanation)
})
}
export let defaultNode = nodeValue => ({
nodeValue,
// eslint-disable-next-line
jsx: nodeValue => <span className="value">{nodeValue}</span>,
isDefault: true
})
export let parseObject = (recurse, objectShape, value) => {
let recurseOne = key => defaultValue => {
if (value[key] == null && !defaultValue)
throw new Error(
`Il manque une clé '${key}' dans ${JSON.stringify(value)} `
)
return value[key] != null ? recurse(value[key]) : defaultValue
}
let transforms = fromPairs(
map(k => [k, recurseOne(k)], keys(objectShape)) as any
)
return evolve(transforms as any, objectShape)
}
export let evaluateObject = (objectShape, effect) => (
cache,
situationGate,
parsedRules,
node
) => {
const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules)
const evaluations = map(evaluate, node.explanation)
const temporalExplanations = mapTemporal(
Object.fromEntries,
concatTemporals(
Object.entries(evaluations).map(([key, node]) =>
zipTemporals(pureTemporal(key), liftTemporalNode(node))
)
)
)
const temporalExplanation = mapTemporal(explanations => {
const evaluation = effect(explanations, cache, situationGate, parsedRules)
return {
...evaluation,
explanation: {
...explanations,
...evaluation.explanation
}
}
}, temporalExplanations)
const sameUnitTemporalExplanation: Temporal<EvaluatedNode<
number
>> = convertNodesToSameUnit(
temporalExplanation.map(x => x.value),
cache._meta.contextRule,
node.name
).map((node, i) => ({
...temporalExplanation[i],
value: simplifyNodeUnit(node)
}))
const temporalValue = mapTemporal(
({ nodeValue }) => nodeValue,
sameUnitTemporalExplanation
)
const nodeValue = temporalAverage(temporalValue)
const baseEvaluation = {
...node,
nodeValue,
unit: sameUnitTemporalExplanation[0].value.unit,
explanation: evaluations,
missingVariables: mergeAllMissing(Object.values(evaluations))
}
if (sameUnitTemporalExplanation.length === 1) {
return {
...baseEvaluation,
explanation: sameUnitTemporalExplanation[0].value.explanation
}
}
return {
...baseEvaluation,
temporalValue,
temporalExplanation
}
}

View File

@ -6,25 +6,33 @@
# @preprocessor esmodule
@{%
const {string, filteredVariable, date, variable, variableWithConversion, binaryOperation, unaryOperation, boolean, number, numberWithUnit } = require('./grammarFunctions')
const {
string, filteredVariable, date, variable, variableWithConversion,
temporalNumericValue, binaryOperation, unaryOperation, boolean, number,
numberWithUnit
} = require('./grammarFunctions')
const moo = require("moo");
const dateRegexp = /(?:(?:0?[1-9]|[12][0-9]|3[01])\/)?(?:0?[1-9]|1[012])\/\d{4}/
const dateRegexp = `(?:(?:0?[1-9]|[12][0-9]|3[01])\\/)?(?:0?[1-9]|1[012])\\/\\d{4}`
const letter = '[a-zA-Z\u00C0-\u017F€$%]';
const letterOrNumber = '[a-zA-Z\u00C0-\u017F0-9\']';
const word = `${letter}(?:[\-']?${letterOrNumber}+)*`;
const word = `${letter}(?:[-']?${letterOrNumber}+)*`;
const wordOrNumber = `(?:${word}|${letterOrNumber}+)`
const words = `${word}(?:[\\s]?${wordOrNumber}+)*`
const periodWord = `\\| ${word}(?:[\\s]${word})*`
const numberRegExp = '-?(?:[1-9][0-9]+|[0-9])(?:\\.[0-9]+)?';
const lexer = moo.compile({
date: dateRegexp,
'(': '(',
')': ')',
'[': '[',
']': ']',
comparison: ['>','<','>=','<=','=','!='],
infinity: 'Infinity',
colon: " : ",
date: new RegExp(dateRegexp),
periodWord: new RegExp(periodWord),
words: new RegExp(words),
number: new RegExp(numberRegExp),
string: /'[ \t\.'a-zA-Z\-\u00C0-\u017F0-9 ]+'/,
@ -33,7 +41,7 @@ const lexer = moo.compile({
dot: ' . ',
'.': '.',
letterOrNumber: new RegExp(letterOrNumber),
space: { match: /[\s]+/, lineBreaks: true }
space: { match: /[\s]+/, lineBreaks: true },
});
const join = (args) => ({value: (args.map(x => x && x.value).join(""))})
@ -43,12 +51,20 @@ const flattenJoin = ([a, b]) => Array.isArray(b) ? join([a, ...b]) : a
@lexer lexer
main ->
AdditionSubstraction {% id %}
| Comparison {% id %}
| NonNumericTerminal {% id %}
| Negation {% id %}
Comparison {% id %}
| NumericValue {% id %}
| Date {% id %}
| NonNumericTerminal {% id %}
NumericValue ->
AdditionSubstraction {% id %}
| Negation {% id %}
| TemporalNumericValue {% id %}
TemporalNumericValue ->
NumericValue %space %periodWord %space %date {% ([value,,word,,dateString]) => temporalNumericValue(value, word, date([dateString])) %}
| NumericValue %space %periodWord %colon Date {% ([value,,word,,date]) => temporalNumericValue(value, word, date) %}
NumericTerminal ->
Variable {% id %}
| VariableWithUnitConversion {% id %}
@ -59,8 +75,7 @@ Negation ->
"-" %space Parentheses {% unaryOperation('calculation') %}
Parentheses ->
"(" AdditionSubstraction ")" {% ([,e]) => e %}
| "(" Negation ")" {% ([,e]) => e %}
"(" NumericValue ")" {% ([,e]) => e %}
| NumericTerminal {% id %}
Date ->
@ -92,6 +107,7 @@ VariableWithUnitConversion ->
Filter -> "." %words {% ([,filter]) => filter %}
FilteredVariable -> Variable %space Filter {% filteredVariable %}
AdditionSubstraction ->
AdditionSubstraction %space %additionSubstraction %space MultiplicationDivision {% binaryOperation('calculation') %}
| MultiplicationDivision {% id %}

View File

@ -2,6 +2,7 @@
The advantage of putting them here is to get prettier's JS formatting, since Nealrey doesn't support it https://github.com/kach/nearley/issues/310 */
import { normalizeDateString } from 'Engine/date'
import { parseUnit } from 'Engine/units'
import { parsePeriod } from './temporal'
export let binaryOperation = operationType => ([A, , operator, , B]) => ({
[operator]: {
@ -25,6 +26,13 @@ export let variableWithConversion = ([{ variable }, , unit]) => ({
unitConversion: { explanation: variable, unit: parseUnit(unit.value) }
})
export let temporalNumericValue = (variable, word, date) => ({
temporalValue: {
explanation: variable,
period: parsePeriod(word.value.slice(2), date)
}
})
export let variable = ([firstFragment, nextFragment], _, reject) => {
const fragments = [firstFragment, ...nextFragment].map(({ value }) => value)
if (!nextFragment.length && ['oui', 'non'].includes(firstFragment)) {

View File

@ -11,7 +11,6 @@ import './Variations.css'
let Comp = function Variations({ nodeValue, explanation, unit }) {
let [expandedVariation, toggleVariation] = useState(null)
const { i18n } = useTranslation()
return (
<ShowValuesConsumer>
{showValues => (
@ -64,7 +63,7 @@ let Comp = function Variations({ nodeValue, explanation, unit }) {
)}
{(expandedVariation === i || satisfied || !showValues) && (
<div style={{ margin: '1rem 0' }}>
{condition && (
{!condition.isDefault && (
<div
style={{
display: 'flex',
@ -89,7 +88,7 @@ let Comp = function Variations({ nodeValue, explanation, unit }) {
satisfied
})}
>
{condition ? (
{!condition.isDefault ? (
<Trans>Alors</Trans>
) : (
<Trans>Sinon</Trans>

View File

@ -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) => (
<Node
classes="mecanism list complement"
name="complément"
value={nodeValue}
>
<ul className="properties">
<li key="cible">
<span className="key">
<Trans>cible</Trans>:{' '}
</span>
<span className="value">{makeJsx(explanation.cible)}</span>
</li>
<li key="mini">
<span className="key">
<Trans>montant à atteindre</Trans>:{' '}
</span>
<span className="value">{makeJsx(explanation.montant)}</span>
</li>
</ul>
</Node>
)
}
}
export let mecanismSynchronisation = (recurse, k, v) => {
let evaluate = (cache, situationGate, parsedRules, node) => {
let APIExplanation = evaluateNode(

View File

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

View File

@ -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 }) {
</Node>
)
}
const today = new Date()
const todayString = normalizeDate(
today.getFullYear(),
today.getMonth() + 1,
today.getDate()
)
const todayString = convertToString(new Date())
const objectShape = {
depuis: defaultNode(todayString),

View File

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

View File

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

View File

@ -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<Evaluation<number>>,
unit: Unit
): Temporal<Evaluation<number>> {
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<typeof parse>
) {
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<Evaluation<number>>) {
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<Evaluation<number>>
}
const evaluation = evaluate(node.explanation.rule)
const temporalValue = evaluation.temporalValue
const evaluationWithRegularisation = groupByYear(
temporalValue as Temporal<Evaluation<number>>
)
.map(regulariseYear)
.flat()
return {
...node,
temporalValue: evaluationWithRegularisation,
explanation: evaluation,
nodeValue: temporalAverage(temporalValue),
missingVariables: evaluation.missingVariables,
unit: evaluation.unit
}
}

View File

@ -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<number> =
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 &&
<number>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 < <number>plafondValue
}
return [[...tranches, tranche], tranche.isActive]

View File

@ -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<typeof parseVariableTemporelle>
) {
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<number>, 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
}
}

View File

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

View File

@ -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<Variation> }
) => {
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<typeof parse>
) {
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 })
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

438
source/engine/temporal.ts Normal file
View File

@ -0,0 +1,438 @@
import {
convertToDate,
getDifferenceInDays,
getDifferenceInMonths,
getDifferenceInYears,
getRelativeDate,
getYear
} from 'Engine/date'
import { Unit } from './units'
export type Period<T> = {
start: T | null
end: T | null
}
export function parsePeriod<Date>(word: string, date: Date): Period<Date> {
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> = T | false | null
export type EvaluatedNode<T> = {
unit: Unit
nodeValue: Evaluation<T>
temporalValue?: Temporal<Evaluation<T>>
explanation?: Object
missingVariables?: Object
}
export type TemporalNode<T> = Temporal<{ nodeValue: Evaluation<T> }>
export type Temporal<T> = Array<Period<string> & { value: T }>
export function narrowTemporalValue<T>(
period: Period<string>,
temporalValue: Temporal<Evaluation<T>>
): Temporal<Evaluation<T>> {
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<T>(
value: Evaluation<T>,
period: Period<string> = { start: null, end: null }
): Temporal<Evaluation<T>> {
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<T>(value: T): Temporal<T> {
return [{ start: null, end: null, value }]
}
export function mapTemporal<T1, T2>(
fn: (value: T1) => T2,
temporalValue: Temporal<T1>
): Temporal<T2> {
return temporalValue.map(({ start, end, value }) => ({
start,
end,
value: fn(value)
}))
}
export function sometime<T1>(
fn: (value: T1) => boolean,
temporalValue: Temporal<T1>
): boolean {
return temporalValue.some(({ start, end, value }) => fn(value))
}
export function liftTemporal2<T1, T2, T3>(
fn: (value1: T1, value2: T2) => T3,
temporalValue1: Temporal<T1>,
temporalValue2: Temporal<T2>
): Temporal<T3> {
return mapTemporal(
([a, b]) => fn(a, b),
zipTemporals(temporalValue1, temporalValue2)
)
}
export function concatTemporals<T, U>(
temporalValues: Array<Temporal<T>>
): Temporal<Array<T>> {
return temporalValues.reduce(
(values, value) => liftTemporal2((a, b) => [...a, b], values, value),
pureTemporal([]) as Temporal<Array<T>>
)
}
export function liftTemporalNode<T>(node: EvaluatedNode<T>): TemporalNode<T> {
const { temporalValue, ...baseNode } = node
if (!temporalValue) {
return pureTemporal(baseNode)
}
return mapTemporal(
nodeValue => ({
...baseNode,
nodeValue
}),
temporalValue
)
}
export function zipTemporals<T1, T2>(
temporalValue1: Temporal<T1>,
temporalValue2: Temporal<T2>,
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<T>(
fn: (date: string) => string,
temporal: Temporal<T>
): Temporal<T> {
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<T>)
}
function splitEndsAt<T>(
fn: (date: string) => string,
temporal: Temporal<T>
): Temporal<T> {
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<T>)
}
export function groupByYear<T>(temporalValue: Temporal<T>): Array<Temporal<T>> {
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<Temporal<T>>)
.reverse()
)
}
function simplify<T>(temporalValue: Temporal<T>): Temporal<T> {
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<Evaluation<number>>,
unit?: Unit
): Evaluation<number> {
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<number>
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<Evaluation<number>>,
unit: Unit
): Evaluation<number> {
temporalValue = temporalValue.filter(({ value }) => value !== false)
if (!temporalValue.length) {
return false
}
if (temporalValue.some(({ value }) => value == null)) {
return null
}
const temporalNumber = temporalValue as Temporal<number>
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)
}

View File

@ -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<number>
): Evaluation<number>
export function convertUnit(from, to, value): any {
if (!areUnitConvertible(from, to)) {
throw new Error(
`Impossible de convertir l'unité '${serializeUnit(

View File

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

View File

@ -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<Unit>
defaultUnit?: Unit
}
function currentExample(state: Example = null, action: Action): Example {
@ -155,7 +156,7 @@ export type SimulationConfig = Partial<{
bloquant: Array<DottedName>
situation: Simulation['situation']
branches: Array<{ nom: string; situation: SimulationConfig['situation'] }>
'unités par défaut': [string]
'unité par défaut': string
}>
type Situation = Partial<Record<DottedName, any>>
@ -165,7 +166,7 @@ export type Simulation = {
hiddenControls: Array<string>
situation: Situation
initialSituation: Situation
defaultUnits: [string]
defaultUnit: string
foldedSteps: Array<DottedName>
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<DottedName>,
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

View File

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

View File

@ -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<HTMLDivElement>(null)
return (

View File

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

View File

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

19
test/date.test.js Normal file
View File

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

View File

@ -10,6 +10,7 @@ import {
let state = {
rules,
simulation: {
defaultUnit: '€/mois',
config: salariéConfig,
situation: {
'contrat salarié . rémunération . brut de base': '2300',

View File

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

View File

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

View File

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

View File

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

View File

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

132
test/period.test.js Normal file
View File

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

View File

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

132
test/temporal.test.js Normal file
View File

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