Merge pull request #15 from sgmap/new-cotisations

Calcule le salaire net et le cout du travail à partir du brut
pull/44/head
Laurent Bossavit 2017-07-18 17:17:33 +02:00 committed by GitHub
commit 709a15cec4
39 changed files with 1419 additions and 543 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
.tmp
node_modules/
dist/
.DS_Store

View File

@ -1,41 +0,0 @@
import {expect} from 'chai'
import {enrichRule, collectMissingVariables, getObjectives} from '../source/engine/rules'
import {analyseSituation} from '../source/engine/traverse'
let stateSelector = (state, name) => null
describe('enrichRule', function() {
it('should extract the type of the rule', function() {
let rule = {cotisation:{}}
expect(enrichRule(rule)).to.have.property('type','cotisation')
});
it('should extract the dotted name of the rule', function() {
let rule = {espace:"contrat salarié", nom: "CDD"}
expect(enrichRule(rule)).to.have.property('name','CDD')
expect(enrichRule(rule)).to.have.property('dottedName','contrat salarié . CDD')
});
it('should render Markdown in sub-questions', function() {
let rule = {"sous-question":"**wut**"}
expect(enrichRule(rule)).to.have.property('subquestion','<p><strong>wut</strong></p>\n')
});
});
describe('collectMissingVariables', function() {
it('should derive objectives from the root rule', function() {
let rawRules = [
{nom: "startHere", formule: {somme: [3259, "dix"]}, espace: "top"},
{nom: "dix", formule: "cinq", espace: "top"},
{nom: "cinq", espace: "top"}],
rules = rawRules.map(enrichRule),
situation = analyseSituation(rules,"startHere")(stateSelector),
result = getObjectives(situation)
expect(result).to.have.lengthOf(1)
expect(result[0]).to.have.property('name','dix')
});
});

View File

@ -34,7 +34,7 @@
},
"devDependencies": {
"autoprefixer": "^7.1.1",
"babel-cli": "^6.23.0",
"babel-cli": "^6.24.1",
"babel-core": "^6.24.1",
"babel-eslint": "^7.2.3",
"babel-loader": "^7.0.0",
@ -42,10 +42,12 @@
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-do-expressions": "^6.22.0",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-plugin-webpack-alias": "^2.1.2",
"babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.4.0",
"babel-preset-react": "^6.24.1",
"chai": "^4.0.2",
"chokidar": "^1.7.0",
"core-js": "^2.4.1",
"css-loader": "^0.28.1",
"eslint": "^3.19.0",
@ -54,6 +56,7 @@
"file-loader": "^0.11.1",
"html-loader": "^0.4.5",
"img-loader": "^2.0.0",
"jsdom": "^11.0.0",
"json-loader": "^0.5.4",
"mocha": "^3.4.2",
"mocha-webpack": "^0.7.0",
@ -74,6 +77,7 @@
"start": "node source/server.js",
"compile": "NODE_ENV='production' webpack --config source/webpack.config.js",
"surge": "npm run compile && surge --domain scientific-wish.surge.sh -p ./ && rm -rf dist/",
"test": "mocha-webpack --webpack-config source/webpack.config.js --require source-map-support/register \"__tests__/**/*.test.js\""
"test": "mocha-webpack --webpack-config source/webpack.config.js --require source-map-support/register --require test/helpers/browser.js \"test/**/*.test.js\"",
"test-fast": "babel-node --presets babel-preset-flow,babel-preset-env --plugins transform-class-properties test/helpers/runner.js -w"
}
}

View File

@ -46,7 +46,7 @@
références:
La mojoration de la contribution chômage: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/lassurance-chomage-et-lags/la-majoration-de-la-contribution.html
La majoration de la contribution chômage: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/lassurance-chomage-et-lags/la-majoration-de-la-contribution.html
Circulaire Unédic: http://www.unedic.org/sites/default/files/ci201317_1.pdf
notes: |

View File

@ -1,71 +0,0 @@
- Cotisation: AGFF
attributs:
branche: retraite
type de retraite: complémentaire
destinataire: AGFF
description: |
Cotisation de retraite complémentaire
(Cotisation pour l'Association pour la Gestion du Fonds de Financement de lAGIRC et de lARRCO)
référence: http://www.agirc-arrco.fr/entreprises/gerer-les-salaries/calcul-des-cotisations/
notes: |
Attention: les tranches du barème sont différentes pour les cadres et non-cadres, en valeur et en nombres.
formule:
barème:
assiette: assiette cotisations sociales
composantes:
- attributs:
dû par: employeur
variations:
- si: statut cadre = non
tranches:
- taux:
2001-04: 0.8%
- seuil: 1 * plafond sécurité sociale
taux:
2001-04: 0.9%
- seuil: 3 * plafond sécurité sociale
taux: 0%
- si: statut cadre = oui
tranches:
- taux:
2001-04: 1.2%
- seuil: 1 * plafond sécurité sociale
taux:
2001-04: 1.3%
- seuil: 4 * plafond sécurité sociale
taux:
2016: 1.3%
2001-04: 0%
- seuil: 8 * plafond sécurité sociale
taux: 0%
- attributs:
dû par: salarié
variations:
- si: statut cadre = non
tranches:
- taux:
2001-04: 0.8%
- seuil: 1 * plafond sécurité sociale
taux:
2001-04: 0.9%
- seuil: 3 * plafond sécurité sociale
taux: 0%
- si: statut cadre = oui
tranches:
- taux:
2001-04: 0.8%
- seuil: 1
taux:
2001-04: 0.9%
- seuil: 4
taux:
2016-01: 0.9%
2001-04: 0%
- seuil: 8
taux: 0%

View File

@ -1,82 +0,0 @@
- Cotisation: Maladie
attributs:
branche: maladie
initiales: MMID-CSA
description: Cotisations de la branche maladie
références: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/la-cotisation-maladie---maternit.html
formule:
multiplication:
assiette: assiette cotisations sociales
composantes:
- attributs: # On va ici surcharger la Cotisation incomplète définie plus haut
composante: maladie, maternité, invalidité, décès
dû par: employeur
taux:
2017-01: 12.89%
2016-01: 12.84%
1992-07: 12.8%
- attributs:
composante: Contribution Solidarité Autonomie
abbréviation: CSA
dû par: employeur
références:
- https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/la-contribution-solidarite-auton.html
- https://www.service-public.fr/professionnels-entreprises/vosdroits/F32872
formule:
taux:
2016-01: 3%
2004-07: 3%
- attributs:
composante: maladie, maternité, invalidité, décès
dû par: salarié
formule:
taux:
2014-01: 0.075%
1998-01: 0.075%
1997-01: 0.55%
1993-07: 0.68%
- attributs:
composante: maladie, maternité, invalidité, décès
dû par: salarié
applicable si: régime géographique = Alsace-Moselle
description: Complément de cotisation maladie spécifique au régime de sécurité sociale d'Alsace-Moselle
référence: https://baseircantec.retraites.fr/cotisations-assurance-maladie-alsace-moselle.html
formule:
# base: selon cette source, la base est l'assiette de la CSG : https://baseircantec.retraites.fr/cotisations-assurance-maladie-alsace-moselle.html
# information non retrouvée ailleurs
taux:
2012-01: 1.5%
2008-01: 1.6%
2007-07: 1.7%
2006-01: 1.8%
2003-01: 1.7%
1999-07: 1.5%
1998-07: 1.25%
1994-01: 1%
1989-09: 0.75%
exception: # équivaut à un variations: si [exception] / si [cas normal]
si: régime = agricole
2014-01: 1.1%
2011-07: 1.2%
2008-07: 1.3%
2007-01: 1.4%
2003-01: 1.5%
# - si: Activité = Indépendant
# description: Cotisations maladie et maternité
# Cotisation:
# branche: maladie
# collecteur: RSI
# calendrier: RSI
# formule:
# multiplication:
# assiette: revenus professionnels # l'assiette différente fait qu'il n'y a pas vraiment d'intérêt de mettre en commun avec Activité = Salarié
# taux: 0.065

View File

@ -0,0 +1,66 @@
- espace: contrat salarié
nom: AGFF
cotisation:
branche: retraite
type de retraite: complémentaire
destinataire: AGFF
description: |
Cotisation de retraite complémentaire
(Cotisation pour l'Association pour la Gestion du Fonds de Financement de lAGIRC et de lARRCO)
référence: http://www.agirc-arrco.fr/entreprises/gerer-les-salaries/calcul-des-cotisations/
notes: |
Attention: les tranches du barème sont différentes pour les cadres et non-cadres, en valeur et en nombres.
formule:
barème:
assiette: assiette cotisations sociales
multiplicateur des tranches: plafond sécurité sociale
composantes:
- attributs:
dû par: employeur
variations:
- si: statut cadre = non
tranches:
- en-dessous de: 1
taux: 0.8%
- de: 1
à: 3
taux: 0.9%
- en-dessous de: 3
taux: 0%
- si: statut cadre = oui
tranches:
- taux:
- en-dessous de: 1
taux: 1.2%
- de: 1
à: 8
taux: 1.3%
- au-dessus de: 8
taux: 0%
- attributs:
dû par: salarié
variations:
- si: statut cadre = non
tranches:
- en-dessous de: 1
taux: 0.8%
- de: 1
à: 3
taux: 0.9%
- au-dessus de: 3
taux: 0%
- si: statut cadre = oui
tranches:
- en-dessous de: 1
taux: 0.8%
- de: 1
à: 8
taux: 0.9%
- au-dessus de: 8
taux: 0%

View File

@ -1,7 +1,6 @@
- Cotisation: GMP
attributs:
- espace: contrat salarié
nom: GMP
cotisation:
branche: retraite
type de retraite: complémentaire
destinataire: AGIRC
@ -14,51 +13,23 @@
si > PSS alors le mec va payer une cotisation AGIRC sur la tranche B, et la GMP sera le complément pour arriver à un montant total = cotisation #forfaitaire GMP
Autrement dit, si agirc < cotisation forfaitaire, GMP = complément
concerne: catégorie salarié = cadre
# TODO On pourrait aussi se dire que cette formule est un complément de AGIRC,
# donc que les conditions d'applicabilité d'AGIRC n'ont pas à être répétées
non applicable si: ≠ statut cadre
complément:
# TODO harmoniser la syntaxe de ce 'complément' avec les systèmes de réduction de cotisation. C'est pareil avec une addition finalement
# cette cotisation vient compléter la cotisation cible, à hauteur du montant spécifié
cible: agirc
formule:
composantes:
- attributs:
dû par: employeur
montant:
2017: 43.67
2016: 42.23
2014: 41.17
2013: 41.13
2012: 40.74
2011: 39.84
2010: 38.99
2009: 38.48
2008: 37.81
2007: 36.57
2006: 35.27
2005: 34.58
2004: 33.75
2003: 32.97
2002: 32.42
- attributs:
dû par: salarié
montant:
2017: 26.71
2016: 25.84
2014: 25.17
2013: 25.13
2012: 24.90
2011: 24.35
2010: 23.82
2009: 23.52
2008: 23.11
2007: 22.35
2006: 21.56
2005: 20.75
2004: 20.25
2003: 19.78
2002: 19.45
complément:
# TODO harmoniser la syntaxe de ce 'complément' avec les systèmes de réduction de cotisation. C'est pareil avec une addition finalement
# cette cotisation vient compléter la cotisation cible, à hauteur du montant spécifié
cible: agirc
composantes:
- attributs:
dû par: employeur
montant: 43.76
- attributs:
dû par: salarié
montant: 26.71
# salaire charnière, inutile avec le méchanisme de complément.
# C'est le salaire pour lequel le salarié acquiert 120 points AGIRC

View File

@ -11,7 +11,7 @@
non applicable si: ≠ statut cadre
formule:
barème:
assiette: salaire de base #TODO devrait être assiette cotisations sociales. Mais elle contient les primes CDD
assiette: assiette cotisations sociales #TODO devrait être assiette cotisations sociales. Mais elle contient les primes CDD
multiplicateur des tranches: plafond sécurité sociale
composantes:
- attributs:

View File

@ -0,0 +1,29 @@
- espace: contrat salarié
nom: maladie
cotisation:
branche: maladie
description: Cotisations de la branche maladie
référence: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/la-cotisation-maladie---maternit.html
formule:
multiplication:
assiette: assiette cotisations sociales
composantes:
- attributs: # On va ici surcharger la Cotisation incomplète définie plus haut
composante: maladie, maternité, invalidité, décès
dû par: employeur
taux: 12.89%
- attributs:
composante: Contribution Solidarité Autonomie
abbréviation: CSA
dû par: employeur
références:
- https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/la-contribution-solidarite-auton.html
- https://www.service-public.fr/professionnels-entreprises/vosdroits/F32872
taux: 0.3%
- attributs:
composante: maladie, maternité, invalidité, décès
dû par: salarié
taux: 0.75%

View File

@ -0,0 +1,28 @@
- espace: contrat salarié
nom: vieillesse
cotisation:
branche: retraite
collecteur: URSSAF
destinataire: CNAV
# CTP: 100
description: Cotisation au régime de retraite de base des salariés.
formule:
multiplication:
assiette: assiette cotisations sociales
composantes:
- attributs:
dû par: salarié
composantes:
- nom: non plafonnée
taux: 0.4%
- nom: plafonnée
plafond: plafond sécurité sociale
taux: 6.90%
- attributs:
dû par: employeur
composantes:
- nom: non plafonnée
taux: 1.9%
- nom: plafonnée
plafond: plafond sécurité sociale
taux: 8.55%

View File

@ -1,59 +0,0 @@
- Cotisation: Vieillesse
attributs:
branche: retraite
type de retraite: de base
collecteur: URSSAF
destinataire: CNAV
# CTP: 100
description: Cotisation au régime de retraite de base des salariés.
formule:
multiplication:
assiette: assiette cotisations sociales
composantes:
- attributs:
dû par: salarié
composantes:
- nom: non plafonnée
formule:
taux:
2018-01: 0.4%
2017-01: 0.4%
2016-01: 0.35%
2015-01: 0.3%
2014-01: 0.25%
2004-07: 0.1%
- nom: plafonnée
formule:
plafond: plafond sécurité sociale
taux:
2017-01: 6.90%
2016-01: 6.90%
2015-01: 6.85%
2014-01: 6.80%
2012-11: 6.75%
2006-01: 6.65%
1993-07: 6.55%
- attributs:
dû par: employeur
composantes:
- nom: non plafonnée
formule:
taux:
2018-01: 1.9%
2017-01: 1.9%
2016-01: 1.85%
2015-01: 1.8%
2014-01: 1.75%
1991-02: 1.6%
- nom: plafonnée
formule:
plafond: plafond sécurité sociale
taux:
2017-01: 8.55%
2016-01: 8.55%
2015-01: 8.5%
2014-01: 8.45%
2012-11: 8.4%
2006-01: 8.3%
1979-01: 8.2%

View File

@ -21,10 +21,18 @@
- CDD . prime fin de contrat #indemnité
- CDD . compensation congés payés #indemnité
# TODO - apparement new-cotisations change l'ordre de priorité des questions et nous fait
# retomber sur "salaire de base", cette modif est un workaround en attendant d'y voir plus clair
- espace: contrat salarié
nom: salaire de base
question: Quel est le salaire de base ?
description: Le salaire de base est le salaire brut régulier inscrit dans le contrat. C'est le salaire de négociation entre le salarié et l'employeur. Des primes viendront éventuellement le compléter, on parlera alors de salaire brut.
titre: Salaire brut
question: Quel est le salaire brut ?
description: |
C'est le salaire de négociation du contrat de travail en France.
Il peut être vu comme :
- la somme du salaire net et des cotisations sociales salariales retenues sur le bulletin de paie d'un salarié
- ou comme les sommes perçues par le salarié au titre de son contrat de travail, avant retenues sociales et fiscales.
format: euros
suggestions:
salaire médian: 2300
@ -43,9 +51,9 @@
format: euros
# TODO En attendant que l'UI devienne plus intelligente, c'est confondu avec le salaire de base.
# intelligente : il faudrait demander : `salaire brut`, puis un bouton `qu'est-ce que c'est` pour nous guider et décortiquer la formule
# formule:
# somme:
# - salaire de base
formule:
somme:
- salaire de base
# - primes
# - indemnités
suggestions:
@ -65,19 +73,42 @@
# type de période: mensuel
formule: 3269
- espace: contrat salarié
nom: cotisations
description: |
Les cotisations contributives et non contributives
formule:
somme:
- maladie
- vieillesse
- espace: contrat salarié
nom: salaire net
description: |
C'est, en gros, le salaire brut moins les cotisations sociales. Ce salaire est plus important que le brut car c'est ce que le salrié reçoit sur son compte bancaire, et pourtant, le brut est très utilisé lors des négociations salariales.
formule:
# TODO à compléter
somme: #TODO à l'avenir, exprimer une somme sous forme de requête
- APEC
- AGIRC
formule: salaire brut - cotisations (salarié)
- espace: contrat salarié
nom: coût du travail
description: |
C'est le salaire de base augmenté des cotisations patronales.
formule: salaire brut + cotisations (employeur)
- espace: contrat salarié
nom: Salaire
description: |
Le coût du travail salarial
formule:
somme: #TODO à l'avenir, exprimer une somme par requête de type : obligation applicable au CDD
- salaire net
- coût du travail
simulateur:
titre: Simulateur de coût d'embauche
sous-titre: Découvrir le coût d'embauche ou le salaire réel
résultats: Le salaire net à partir du brut ou vice-versa, et les cotisations
introduction:
motivation: Découvrez le vrai coût du travail

View File

@ -12,6 +12,7 @@
"transform-decorators-legacy",
"transform-do-expressions",
"transform-object-rest-spread",
"transform-class-properties"
"transform-class-properties",
["webpack-alias", { "config": "./source/webpack.config.js" }]
]
}

View File

@ -1,10 +1,12 @@
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {rules, findRuleByDottedName} from '../engine/rules'
import './Aide.css'
import marked from 'Engine/marked'
import {rules, findRuleByDottedName} from 'Engine/rules'
import {EXPLAIN_VARIABLE} from '../actions'
import References from './rule/References'
import marked from '../engine/marked'
import './Aide.css'
@connect(
state =>

View File

@ -1,7 +1,7 @@
import R from 'ramda'
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import R from 'ramda'
import marked from '../engine/marked'
import marked from 'Engine/marked'
// On ajoute à la section la possibilité d'ouvrir un panneau d'explication des termes.
// Il suffit à la section d'appeler une fonction fournie en lui donnant du JSX

View File

@ -1,8 +1,8 @@
import React, {Component} from 'react'
import './HomeSyso.css'
import {searchRules, encodeRuleName} from '../engine/rules.js'
import {Link} from 'react-router-dom'
import R from 'ramda'
import React, {Component} from 'react'
import {Link} from 'react-router-dom'
import {searchRules, encodeRuleName} from 'Engine/rules.js'
import './HomeSyso.css'
export default class Home extends Component {
state = {

View File

@ -1,13 +1,15 @@
import R from 'ramda'
import React, { Component } from 'react'
import classNames from 'classnames'
import {Link} from 'react-router-dom'
import {connect} from 'react-redux'
import { withRouter } from 'react-router'
import R from 'ramda'
import './Results.css'
import {capitalise0} from '../utils'
import {computeRuleValue} from 'Engine/traverse'
import {encodeRuleName, getObjectives} from 'Engine/rules'
import {encodeRuleName} from 'Engine/rules'
import {getObjectives} from 'Engine/generateQuestions'
let fmt = new Intl.NumberFormat('fr-FR').format
let humanFigure = decimalDigits => value => fmt(value.toFixed(decimalDigits))

View File

@ -1,5 +1,5 @@
import React, {Component} from 'react'
import HoverDecorator from 'Components/HoverDecorator'
import HoverDecorator from './HoverDecorator'
import 'whatwg-fetch'
import {connect} from 'react-redux'
import './Satisfaction.css'

View File

@ -1,18 +1,19 @@
import R from 'ramda'
import React, {Component} from 'react'
import Helmet from 'react-helmet'
import {reduxForm, formValueSelector, reset} from 'redux-form'
import {connect} from 'react-redux'
import {START_CONVERSATION} from '../actions'
import R from 'ramda'
import {Redirect, Link, withRouter} from 'react-router-dom'
import classNames from 'classnames'
import {START_CONVERSATION} from '../actions'
import Aide from './Aide'
import {createMarkdownDiv} from 'Engine/marked'
import {rules, findRuleByName, decodeRuleName} from 'Engine/rules'
import 'Components/conversation/conversation.css'
import 'Components/Simulateur.css'
import classNames from 'classnames'
import './conversation/conversation.css'
import './Simulateur.css'
import {capitalise0} from '../utils'
import Satisfaction from 'Components/Satisfaction'
import Helmet from 'react-helmet'
import Satisfaction from './Satisfaction'
let situationSelector = formValueSelector('conversation')

View File

@ -4,7 +4,7 @@ import './Explicable.css'
import HoverDecorator from '../HoverDecorator'
import {connect} from 'react-redux'
import {EXPLAIN_VARIABLE} from '../../actions'
import {rules, findRuleByDottedName} from '../../engine/rules'
import {rules, findRuleByDottedName} from 'Engine/rules'
@connect(state => ({explained: state.explainedVariable}), dispatch => ({

View File

@ -1,7 +1,7 @@
import React from 'react'
import R from 'ramda'
import references from 'Règles/ressources/références/références.yaml'
import './References.css'
import references from 'Règles/ressources/références/références.yaml'
export default ({refs}) => (
<ul className="references">

View File

@ -1,61 +1,23 @@
import React from 'react'
import Explicable from 'Components/conversation/Explicable'
import R from 'ramda'
import Explicable from 'Components/conversation/Explicable'
import Question from 'Components/conversation/Question'
import Input from 'Components/conversation/Input'
import formValueTypes from 'Components/conversation/formValueTypes'
import {analyseSituation} from './traverse'
import {formValueSelector} from 'redux-form'
import { STEP_ACTION, START_CONVERSATION} from '../actions'
import {rules, findRuleByDottedName, collectMissingVariables, deprecated_findVariantsAndRecords} from './rules'
import {rules, findRuleByDottedName, findVariantsAndRecords} from './rules'
export let reduceSteps = (state, action) => {
if (![START_CONVERSATION, STEP_ACTION].includes(action.type))
return state
let rootVariable = action.type == START_CONVERSATION ? action.rootVariable : state.analysedSituation.name
let returnObject = {
...state,
analysedSituation: analyse(rootVariable)(state)
}
if (action.type == START_CONVERSATION) {
return {
...returnObject,
foldedSteps: state.foldedSteps || [],
unfoldedSteps: buildNextSteps(returnObject.analysedSituation)
}
}
if (action.type == STEP_ACTION && action.name == 'fold') {
return {
...returnObject,
foldedSteps: [...state.foldedSteps, R.head(state.unfoldedSteps)],
unfoldedSteps: buildNextSteps(returnObject.analysedSituation)
}
}
if (action.type == STEP_ACTION && action.name == 'unfold') {
let stepFinder = R.propEq('name', action.step),
foldedSteps = R.reject(stepFinder)(state.foldedSteps)
if (foldedSteps.length != state.foldedSteps.length - 1)
throw 'Problème lors du dépliement d\'une réponse'
return {
...returnObject,
foldedSteps,
unfoldedSteps: [R.find(stepFinder)(state.foldedSteps)]
}
}
}
let situationGate = state =>
name => formValueSelector('conversation')(state, name)
let analyse = rootVariable => R.pipe(
export let analyse = rootVariable => R.pipe(
situationGate,
// une liste des objectifs de la simulation (des 'rules' aussi nommées 'variables')
analyseSituation(rules, rootVariable)
@ -77,13 +39,68 @@ let analyse = rootVariable => R.pipe(
missingVariables: {variable: [objectives]}
*/
let buildNextSteps = analysedSituation => {
// On peut travailler sur une somme, les objectifs sont alors les variables de cette somme.
// Ou sur une variable unique ayant une formule, elle est elle-même le seul objectif
export let getObjectives = analysedSituation => {
let formuleType = R.path(["formule", "explanation", "name"])(
analysedSituation
)
let result = formuleType == "somme"
? R.pluck(
"explanation",
R.path(["formule", "explanation", "explanation"])(analysedSituation)
)
: formuleType ? [analysedSituation] : null
return result ? R.reject(R.isNil)(result) : null;
}
// FIXME - this relies on side-effects and the recursion is grossly indiscriminate
let collectNodeMissingVariables = (root, source=root, results=[]) => {
if (
source.nodeValue != null ||
source.shortCircuit && source.shortCircuit(root)
) {
// console.log('nodev or shortcircuit root, source', root, source)
return []
}
if (source['missingVariables']) {
// console.log('root, source', root, source)
results.push(source['missingVariables'])
}
for (var prop in source) {
if (R.is(Object)(source[prop])) {
collectNodeMissingVariables(root, source[prop], results)
}
}
return results
}
export let collectMissingVariables = (groupMethod='groupByMissingVariable') => analysedSituation =>
R.pipe(
getObjectives,
R.chain( v =>
R.pipe(
collectNodeMissingVariables,
R.flatten,
R.map(mv => [v.dottedName, mv])
)(v)
),
//groupBy missing variable but remove mv from value, it's now in the key
R.groupBy(groupMethod == 'groupByMissingVariable' ? R.last : R.head),
R.map(R.map(groupMethod == 'groupByMissingVariable' ? R.head : R.last))
// below is a hand implementation of above... function composition can be nice sometimes :')
// R.reduce( (memo, [mv, dependencyOf]) => ({...memo, [mv]: [...(memo[mv] || []), dependencyOf] }), {})
)(analysedSituation)
export let buildNextSteps = (allRules, analysedSituation) => {
let missingVariables = collectMissingVariables('groupByMissingVariable')(
analysedSituation
)
/*
Parmi les variables manquantes, certaines sont citées dans une règle de type 'une possibilité'.
**On appelle ça des groupes de type 'variante'.**
@ -108,16 +125,23 @@ let buildNextSteps = analysedSituation => {
D'autres variables pourront être regroupées aussi, car elles partagent un parent, mais sans fusionner leurs questions dans l'interface. Ce sont des **groupes de type _record_ **
*/
// This is effectively a missingVariables.groupBy(questionRequired)
// but "questionRequired" does not have a clear specification
// we could look up "what formula is this variable mentioned in, and does it have a question attached"
// the problem is that we parse rules "bottom up", we would therefore need to:
// - parse rules top-down, i.e. analysedSituations = map(treatRuleRoot, rules)
// (might be a problem later on in terms of "big" rulesets, but not now)
// - decorate each rule with "mentions / depends on the following rules"
// - provide a "is mentioned by" query
return R.pipe(
R.keys,
R.reduce(
deprecated_findVariantsAndRecords
, {variantGroups: {}, recordGroups: {}}
),
R.curry(findVariantsAndRecords)(allRules),
// on va maintenant construire la liste des composants React qui afficheront les questions à l'utilisateur pour que l'on obtienne les variables manquantes
R.evolve({
variantGroups: generateGridQuestions(missingVariables),
recordGroups: generateSimpleQuestions(missingVariables),
variantGroups: generateGridQuestions(allRules, missingVariables),
recordGroups: generateSimpleQuestions(allRules, missingVariables),
}),
R.values,
R.unnest,
@ -151,9 +175,9 @@ export let constructStepMeta = ({
let isVariant = R.path(['formule', 'une possibilité'])
let buildVariantTree = relevantPaths => path => {
let buildVariantTree = (allRules, relevantPaths) => path => {
let rec = path => {
let node = findRuleByDottedName(rules, path),
let node = findRuleByDottedName(allRules, path),
variant = isVariant(node),
variants = variant && R.unless(R.is(Array), R.prop('possibilités'))(variant),
shouldBeExpanded = variant && variants.find( v => relevantPaths.find(rp => R.contains(path + ' . ' + v)(rp) )),
@ -171,28 +195,29 @@ let buildVariantTree = relevantPaths => path => {
return rec(path)
}
export let generateGridQuestions = missingVariables => R.pipe(
export let generateGridQuestions = (allRules, missingVariables) => R.pipe(
R.toPairs,
R.map( ([variantRoot, relevantVariants]) =>
({
...constructStepMeta(findRuleByDottedName(rules, variantRoot)),
component: Question,
choices: buildVariantTree(relevantVariants)(variantRoot),
objectives: R.pipe(
R.chain(v => missingVariables[v]),
R.uniq()
)(relevantVariants),
// Mesure de l'impact de cette variable : combien de fois elle est citée par une règle
impact: relevantVariants.reduce((count, next) => count + missingVariables[next].length, 0)
})
R.map( ([variantRoot, relevantVariants]) => {
return ({
...constructStepMeta(findRuleByDottedName(allRules, variantRoot)),
component: Question,
choices: buildVariantTree(allRules, relevantVariants)(variantRoot),
objectives: R.pipe(
R.chain(v => missingVariables[v]),
R.uniq()
)(relevantVariants),
// Mesure de l'impact de cette variable : combien de fois elle est citée par une règle
impact: relevantVariants.reduce((count, next) => count + missingVariables[next].length, 0)
})
}
)
)
export let generateSimpleQuestions = missingVariables => R.pipe(
export let generateSimpleQuestions = (allRules, missingVariables) => R.pipe(
R.values, //TODO exploiter ici les groupes de questions de type 'record' (R.keys): elles pourraient potentiellement êtres regroupées visuellement dans le formulaire
R.unnest,
R.map(dottedName => {
let rule = findRuleByDottedName(rules, dottedName)
let rule = findRuleByDottedName(allRules, dottedName)
if (rule == null) console.log(dottedName)
return Object.assign(
constructStepMeta(rule),

View File

@ -6,6 +6,7 @@ main ->
| Variable {% id %}
| NegatedVariable {% id %}
| ModifiedVariable {% id %}
| FilteredVariable {% id %}
| Comparison {% id %}
Comparison -> Comparable _ ComparisonOperator _ Comparable {% d => ({
@ -21,6 +22,10 @@ ComparisonOperator -> ">" | "<" | ">=" | "<=" | "="
NegatedVariable -> "≠" _ Variable {% d => ({category: 'negatedVariable', variable: d[2] }) %}
FilteredVariable -> Variable _ Filter {% d => ({category: 'filteredVariable', filter: d[2], variable: d[0] }) %}
Filter -> "(" VariableWord ")" {% d =>d[1] %}
# Modificateurs temporels pas utilisés aujourd'hui
ModifiedVariable -> Variable _ Modifier {% d => ({category: 'modifiedVariable', modifier: d[2], variable: d[0] }) %}
@ -38,6 +43,7 @@ CalcExpression -> Term _ ArithmeticOperator _ Term {% d => ({
}) %}
Term -> Variable {% id %}
| FilteredVariable {% id %}
| int {% id %}
ArithmeticOperator -> "+" {% id %}

View File

@ -84,6 +84,11 @@ barème:
L'assiette est décomposée en plusieurs tranches, qui sont multipliées par un taux spécifique.
Les tranches sont très souvent exprimées sous forme de facteurs (par exemple [1, 2, 4]) d'une variable que l'on appelle multiplicateur, par exemple le plafond de la sécurité sociale.
complément:
type: numeric
description: |
Complète une base pour atteindre un seuil minimal
composantes:
type: numeric
description: |

View File

@ -1,14 +1,52 @@
import R from 'ramda'
// This is a mock of webpack's require.context, for testing purposes
if (typeof __webpack_require__ === 'undefined') {
const fs = require('fs');
const path = require('path');
require.context = (base = '.', scanSubDirectories = false, regularExpression = /\.js$/) => {
const yaml = require('js-yaml');
const files = {};
function readDirectory(directory) {
fs.readdirSync(directory).forEach((file) => {
const fullPath = path.resolve(directory, file);
if (fs.statSync(fullPath).isDirectory()) {
if (scanSubDirectories) readDirectory(fullPath);
return;
}
if (!regularExpression.test(fullPath)) return;
files[fullPath] = true;
});
}
readDirectory(path.resolve(__dirname, base));
function Module(file) {
return yaml.safeLoad(fs.readFileSync(file, 'utf8'));
}
Module.keys = () => Object.keys(files);
return Module;
};
}
// This array can't be generated, as the arguments to require.context must be literals :-|
let directoryLoaders =
let directoryLoaders =
[
require.context('../../règles/rémunération-travail/cdd',
true, /([A-Za-z\u00C0-\u017F]|\.|-|_)+.yaml$/),
true, /.yaml$/),
require.context('../../règles/rémunération-travail/entités/ok',
true, /([A-Za-z\u00C0-\u017F]|\.|-|_)+.yaml$/),
true, /.yaml$/),
require.context('../../règles/rémunération-travail/cotisations/ok',
true, /([A-Za-z\u00C0-\u017F]|\.|-|_)+.yaml$/),
true, /.yaml$/),
]
// require.context returns an object which

View File

@ -8,6 +8,62 @@ let transformPercentage = s =>
+s.replace('%', '') / 100
: +s
export let decompose = (recurse, k, v) => {
let
subProps = R.dissoc('composantes')(v),
filter = val(recurse("sys . filter")),
isRelevant = c => !filter || !c.attributs || c.attributs['dû par'] == filter,
composantes = v.composantes.filter(isRelevant).map(c =>
({
... recurse(
R.objOf(k,
{
... subProps,
... R.dissoc('attributs')(c)
})
),
composante: c.nom ? {nom: c.nom} : c.attributs
})
),
nodeValue = anyNull(composantes) ? null
: R.reduce(R.add, 0, composantes.map(val))
return {
nodeValue,
category: 'mecanism',
name: 'composantes',
type: 'numeric',
explanation: composantes,
jsx: <Node
classes="mecanism composantes"
name="composantes"
value={nodeValue}
child={
<ul>
{ composantes.map((c, i) =>
[<li className="composante" key={JSON.stringify(c.composante)}>
<ul className="composanteAttributes">
{R.toPairs(c.composante).map(([k,v]) =>
<li>
<span>{k}: </span>
<span>{v}</span>
</li>
)}
</ul>
<div className="content">
{c.jsx}
</div>
</li>,
i < (composantes.length - 1) && <li className="composantesSymbol"><i className="fa fa-plus-circle" aria-hidden="true"></i></li>
]
)
}
</ul>
}
/>
}
}
export let mecanismOneOf = (recurse, k, v) => {
let result = R.pipe(
R.unless(R.is(Array), () => {throw 'should be array'}),
@ -216,6 +272,10 @@ export let mecanismSum = (recurse,k,v) => {
}
export let mecanismProduct = (recurse,k,v) => {
if (v.composantes) { //mécanisme de composantes. Voir known-mecanisms.md/composantes
return decompose(recurse,k,v)
}
let
mult = (base, rate, facteur, plafond) =>
Math.min(base, plafond) * rate * facteur,
@ -278,58 +338,8 @@ export let mecanismScale = (recurse,k,v) => {
// Sous entendu : barème en taux marginaux.
// A étendre (avec une propriété type ?) quand les règles en contiendront d'autres.
if (v.composantes) { //mécanisme de composantes. Voir known-mecanisms.md/composantes
let
baremeProps = R.dissoc('composantes')(v),
composantes = v.composantes.map(c =>
({
... recurse(
{
barème: {
... baremeProps,
... R.dissoc('attributs')(c)
}
}
),
composante: c.nom ? {nom: c.nom} : c.attributs
})
),
nodeValue = anyNull(composantes) ? null
: R.reduce(R.add, 0, composantes.map(val))
return decompose(recurse,k,v)
return {
nodeValue,
category: 'mecanism',
name: 'composantes',
type: 'numeric',
explanation: composantes,
jsx: <Node
classes="mecanism composantes"
name="composantes"
value={nodeValue}
child={
<ul>
{ composantes.map((c, i) =>
[<li className="composante" key={JSON.stringify(c.composante)}>
<ul className="composanteAttributes">
{R.toPairs(c.composante).map(([k,v]) =>
<li>
<span>{k}: </span>
<span>{v}</span>
</li>
)}
</ul>
<div className="content">
{c.jsx}
</div>
</li>,
i < (composantes.length - 1) && <li className="composantesSymbol"><i className="fa fa-plus-circle" aria-hidden="true"></i></li>
]
)
}
</ul>
}
/>
}
}
if (v['multiplicateur des tranches'] == null)
@ -454,6 +464,48 @@ export let mecanismMax = (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)
}
if (v['cible'] == null)
throw "un complément nécessite une propriété 'cible'"
let cible = recurse(v['cible']),
mini = recurse(v['montant']),
nulled = val(cible) == null,
nodeValue = nulled ? null : R.subtract(val(mini), R.min(val(cible), val(mini)))
return {
type: 'numeric',
category: 'mecanism',
name: 'complément pour atteindre',
nodeValue,
explanation: {
cible,
mini
},
jsx: <Node
classes="mecanism list complement"
name="complément pour atteindre"
value={nodeValue}
child={
<ul className="properties">
<li key="cible">
<span className="key">montant calculé: </span>
<span className="value">{cible.jsx}</span>
</li>
<li key="mini">
<span className="key">montant à atteindre: </span>
<span className="value">{mini.jsx}</span>
</li>
</ul>
}
/>
}
}
export let mecanismError = (recurse,k,v) => {
throw "Le mécanisme est inconnu !"
}

View File

@ -49,7 +49,7 @@ export let decodeRuleName = name => name.replace(/\-/g, ' ')
export let disambiguateRuleReference = (allRules, {ns, name}, partialName) => {
let
fragments = ns.split(' . '), // ex. [CDD . événements . rupture]
fragments = ns ? ns.split(' . ') : [], // ex. [CDD . événements . rupture]
pathPossibilities = // -> [ [CDD . événements . rupture], [CDD . événements], [CDD] ]
R.range(0, fragments.length + 1)
.map(nbEl => R.take(nbEl)(fragments))
@ -83,81 +83,49 @@ export let searchRules = searchInput =>
JSON.stringify(rule).toLowerCase().indexOf(searchInput) > -1)
.map(enrichRule)
export let findRuleByDottedName = (allRules, dottedName) => dottedName &&
allRules.find(rule => rule.dottedName.toLowerCase() == dottedName.toLowerCase())
export let findRuleByDottedName = (allRules, dottedName) => {
let found = dottedName && allRules.find(rule => rule.dottedName.toLowerCase() == dottedName.toLowerCase()),
result = dottedName && dottedName.startsWith("sys .") ?
found || {dottedName: dottedName, nodeValue: null} :
found
return result
}
/*********************************
Autres */
let collectNodeMissingVariables = (root, source=root, results=[]) => {
if (
source.nodeValue != null ||
source.shortCircuit && source.shortCircuit(root)
) {
// console.log('nodev or shortcircuit root, source', root, source)
return []
}
if (source['missingVariables']) {
// console.log('root, source', root, source)
results.push(source['missingVariables'])
}
for (var prop in source) {
if (R.is(Object)(source[prop])) {
collectNodeMissingVariables(root, source[prop], results)
}
}
return results
}
// On peut travailler sur une somme, les objectifs sont alors les variables de cette somme.
// Ou sur une variable unique ayant une formule, elle est elle-même le seul objectif
export let getObjectives = analysedSituation => {
let formuleType = R.path(["formule", "explanation", "name"])(
analysedSituation
)
let result = formuleType == "somme"
? R.pluck(
"explanation",
R.path(["formule", "explanation", "explanation"])(analysedSituation)
)
: formuleType ? [analysedSituation] : null
return result ? R.reject(R.isNil)(result) : null;
}
export let collectMissingVariables = (groupMethod='groupByMissingVariable') => analysedSituation =>
R.pipe(
getObjectives,
R.chain( v =>
R.pipe(
collectNodeMissingVariables,
R.flatten,
R.map(mv => [v.dottedName, mv])
)(v)
),
//groupBy missing variable but remove mv from value, it's now in the key
R.groupBy(groupMethod == 'groupByMissingVariable' ? R.last : R.head),
R.map(R.map(groupMethod == 'groupByMissingVariable' ? R.head : R.last))
// below is a hand implementation of above... function composition can be nice sometimes :')
// R.reduce( (memo, [mv, dependencyOf]) => ({...memo, [mv]: [...(memo[mv] || []), dependencyOf] }), {})
)(analysedSituation)
let isVariant = R.path(['formule', 'une possibilité'])
export let deprecated_findVariantsAndRecords =
({variantGroups, recordGroups}, dottedName, childDottedName) => {
let child = findRuleByDottedName(rules, dottedName),
export let findVariantsAndRecords = (allRules, names) => {
let tag = name => {
let parent = parentName(name),
gramps = parentName(parent),
findV = name => isVariant(findRuleByDottedName(allRules,name))
return findV(gramps) ? {type: "variantGroups", [gramps]:[name]}
: findV(parent) ? {type: "variantGroups", [parent]:[name]}
: {type: "recordGroups", [parent]:[name]}
}
let classify = R.map(tag),
groupByType = R.groupBy(R.prop("type")),
stripTypes = R.map(R.map(R.omit("type"))),
mergeLists = R.map(R.reduce(R.mergeWith(R.concat),{}))
return R.pipe(classify,groupByType,stripTypes,mergeLists)(names)
}
export let findVariantsAndRecords2 =
(allRules, {variantGroups, recordGroups}, dottedName, childDottedName) => {
let child = findRuleByDottedName(allRules, dottedName),
parentDottedName = parentName(dottedName),
parent = findRuleByDottedName(rules, parentDottedName)
parent = findRuleByDottedName(allRules, parentDottedName)
if (isVariant(parent)) {
let grandParentDottedName = parentName(parentDottedName),
grandParent = findRuleByDottedName(rules, grandParentDottedName)
grandParent = findRuleByDottedName(allRules, grandParentDottedName)
if (isVariant(grandParent))
return deprecated_findVariantsAndRecords({variantGroups, recordGroups}, parentDottedName, childDottedName || dottedName)
return findVariantsAndRecords2(allRules, {variantGroups, recordGroups}, parentDottedName, childDottedName || dottedName)
else
return {
variantGroups: R.mergeWith(R.concat, variantGroups, {[parentDottedName]: [childDottedName || dottedName]}),

View File

@ -1,6 +1,8 @@
import React from 'react'
import R from 'ramda'
import classNames from 'classnames'
import {Link} from 'react-router-dom'
import {encodeRuleName} from './rules'
let treatValue = data =>
data == null
@ -40,7 +42,9 @@ export let Leaf = ({classes, name, value}) => (
<span className={classNames(classes, 'leaf')}>
{name &&
<span className="nodeHead">
<span className="name">{name}<NodeValue data={value} /></span>
<Link to={"/regle/" + encodeRuleName(name)} >
<span className="name">{name}<NodeValue data={value} /></span>
</Link>
</span>}
</span>
)

View File

@ -6,7 +6,8 @@ import knownMecanisms from './known-mecanisms.yaml'
import { Parser } from 'nearley'
import Grammar from './grammar.ne'
import {Node, Leaf} from './traverse-common-jsx'
import {mecanismOneOf,mecanismAllOf,mecanismNumericalLogic,mecanismSum,mecanismProduct,mecanismPercentage,mecanismScale,mecanismMax,mecanismError} from "./mecanisms"
import {mecanismOneOf,mecanismAllOf,mecanismNumericalLogic,mecanismSum,mecanismProduct,
mecanismPercentage,mecanismScale,mecanismMax,mecanismError, mecanismComplement} from "./mecanisms"
let nearley = () => new Parser(Grammar.ParserRules, Grammar.ParserStart)
@ -47,6 +48,10 @@ par exemple ainsi : https://github.com/Engelberg/instaparse#transforming-the-tre
*/
// Creates a synthetic variable in the system namespace to signal filtering on components
let withFilter = (rules, filter) =>
R.concat(rules,[{name:"filter", nodeValue:filter, ns:"sys", dottedName: "sys . filter"}])
let fillVariableNode = (rules, rule, situationGate) => (parseResult) => {
let
{fragments} = parseResult,
@ -64,11 +69,12 @@ let fillVariableNode = (rules, rule, situationGate) => (parseResult) => {
),
situationValue = evaluateVariable(situationGate, dottedName, variable),
nodeValue = situationValue
!= null ? situationValue
: !variableIsCalculable
? null
: parsedRule.nodeValue,
nodeValue2 = situationValue
!= null ? situationValue
: !variableIsCalculable
? null
: parsedRule.nodeValue,
nodeValue = dottedName.startsWith("sys .") ? variable.nodeValue : nodeValue2,
explanation = parsedRule,
missingVariables = variableIsCalculable ? [] : (nodeValue == null ? [dottedName] : [])
@ -125,11 +131,15 @@ let treat = (situationGate, rules, rule) => rawNode => {
if (additionnalResults && additionnalResults.length > 0)
throw "Attention ! L'expression <" + rawNode + '> ne peut être traitée de façon univoque'
if (!R.contains(parseResult.category)(['variable', 'calcExpression', 'modifiedVariable', 'comparison', 'negatedVariable']))
if (!R.contains(parseResult.category)(['variable', 'calcExpression', 'filteredVariable', 'comparison', 'negatedVariable']))
throw "Attention ! Erreur de traitement de l'expression : " + rawNode
if (parseResult.category == 'variable')
return fillVariableNode(rules, rule, situationGate)(parseResult)
if (parseResult.category == 'filteredVariable') {
let newRules = withFilter(rules,parseResult.filter)
return fillVariableNode(newRules, rule, situationGate)(parseResult.variable)
}
if (parseResult.category == 'negatedVariable')
return buildNegatedVariable(
fillVariableNode(rules, rule, situationGate)(parseResult.variable)
@ -137,9 +147,12 @@ let treat = (situationGate, rules, rule) => rawNode => {
if (parseResult.category == 'calcExpression') {
let
fillVariable = fillVariableNode(rules, rule, situationGate),
fillFiltered = parseResult => fillVariableNode(withFilter(rules,parseResult.filter), rule, situationGate)(parseResult.variable),
filledExplanation = parseResult.explanation.map(
R.cond([
[R.propEq('category', 'variable'), fillVariableNode(rules, rule, situationGate)],
[R.propEq('category', 'variable'), fillVariable],
[R.propEq('category', 'filteredVariable'), fillFiltered],
[R.propEq('category', 'value'), node =>
R.assoc('jsx', <span className="value">
{node.nodeValue}
@ -260,6 +273,7 @@ let treat = (situationGate, rules, rule) => rawNode => {
'multiplication': mecanismProduct,
'barème': mecanismScale,
'le maximum de': mecanismMax,
'complément': mecanismComplement,
},
action = R.pathOr(mecanismError,[k],dispatch)

View File

@ -1,18 +1,67 @@
import R from 'ramda'
import React from 'react'
import { combineReducers } from 'redux'
import reduceReducers from 'reduce-reducers'
import {reducer as formReducer, formValueSelector} from 'redux-form'
import { euro, months } from './components/conversation/formValueTypes.js'
import { EXPLAIN_VARIABLE, POINT_OUT_OBJECTIVES} from './actions'
import R from 'ramda'
import {rules} from 'Engine/rules'
import {buildNextSteps, generateGridQuestions, generateSimpleQuestions} from 'Engine/generateQuestions'
import computeThemeColours from 'Components/themeColours'
import { STEP_ACTION, START_CONVERSATION, EXPLAIN_VARIABLE, POINT_OUT_OBJECTIVES, CHANGE_THEME_COLOUR} from './actions'
import {reduceSteps, generateGridQuestions, generateSimpleQuestions} from './engine/generateQuestions'
import {analyseSituation} from 'Engine/traverse'
import computeThemeColours from './components/themeColours'
let situationGate = state =>
name => formValueSelector('conversation')(state, name)
let analyse = rootVariable => R.pipe(
situationGate,
// une liste des objectifs de la simulation (des 'rules' aussi nommées 'variables')
analyseSituation(rules, rootVariable)
)
export let reduceSteps = (state, action) => {
if (![START_CONVERSATION, STEP_ACTION].includes(action.type))
return state
let rootVariable = action.type == START_CONVERSATION ? action.rootVariable : state.analysedSituation.name
let returnObject = {
...state,
analysedSituation: analyse(rootVariable)(state)
}
if (action.type == START_CONVERSATION) {
return {
...returnObject,
foldedSteps: state.foldedSteps || [],
unfoldedSteps: buildNextSteps(rules, returnObject.analysedSituation)
}
}
if (action.type == STEP_ACTION && action.name == 'fold') {
return {
...returnObject,
foldedSteps: [...state.foldedSteps, R.head(state.unfoldedSteps)],
unfoldedSteps: buildNextSteps(rules, returnObject.analysedSituation)
}
}
if (action.type == STEP_ACTION && action.name == 'unfold') {
let stepFinder = R.propEq('name', action.step),
foldedSteps = R.reject(stepFinder)(state.foldedSteps)
if (foldedSteps.length != state.foldedSteps.length - 1)
throw 'Problème lors du dépliement d\'une réponse'
return {
...returnObject,
foldedSteps,
unfoldedSteps: [R.find(stepFinder)(state.foldedSteps)]
}
}
}
function themeColours(state = computeThemeColours(), {type, colour}) {
if (type == 'CHANGE_THEME_COLOUR')
if (type == CHANGE_THEME_COLOUR)
return computeThemeColours(colour)
else return state
}
@ -35,7 +84,6 @@ function pointedOutObjectives(state=[], {type, objectives}) {
}
}
export default reduceReducers(
combineReducers({
sessionId: (id = Math.floor(Math.random() * 1000000000000) + '') => id,

View File

@ -0,0 +1,70 @@
import R from 'ramda'
import {expect} from 'chai'
import {rules, enrichRule} from '../source/engine/rules'
import {analyseSituation} from '../source/engine/traverse'
import {buildNextSteps, collectMissingVariables, getObjectives} from '../source/engine/generateQuestions'
let stateSelector = (state, name) => null
describe('collectMissingVariables', function() {
it('should derive objectives from the root rule', function() {
let rawRules = [
{nom: "startHere", formule: {somme: [2, "deux"]}, espace: "sum"},
{nom: "deux", formule: 2, "non applicable si" : "sum . evt . ko", espace: "sum"},
{nom: "evt", espace: "sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"},
{nom: "ko", espace: "sum . evt"}],
rules = rawRules.map(enrichRule),
situation = analyseSituation(rules,"startHere")(stateSelector),
result = getObjectives(situation)
expect(result).to.have.lengthOf(1)
expect(result[0]).to.have.property('name','deux')
});
it('should identify missing variables', function() {
let rawRules = [
{nom: "startHere", formule: {somme: [2, "deux"]}, espace: "sum"},
{nom: "deux", formule: 2, "non applicable si" : "sum . evt . ko", espace: "sum"},
{nom: "evt", espace: "sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"},
{nom: "ko", espace: "sum . evt"}],
rules = rawRules.map(enrichRule),
situation = analyseSituation(rules,"startHere")(stateSelector),
result = collectMissingVariables()(situation)
expect(result).to.have.property('sum . evt . ko')
});
it('should identify missing variables mentioned in expressions', function() {
let rawRules = [
{nom: "startHere", formule: {somme: [2, "deux"]}, espace: "sum"},
{nom: "deux", formule: 2, "non applicable si" : "evt . nyet > evt . nope", espace: "sum"},
{nom: "nope", espace: "sum . evt"},
{nom: "nyet", espace: "sum . evt"}],
rules = rawRules.map(enrichRule),
situation = analyseSituation(rules,"startHere")(stateSelector),
result = collectMissingVariables()(situation)
expect(result).to.have.property('sum . evt . nyet')
expect(result).to.have.property('sum . evt . nope')
});
});
describe('buildNextSteps', function() {
it('should generate questions', function() {
let rawRules = [
{nom: "sum", formule: {somme: [2, "deux"]}, espace: "top"},
{nom: "deux", formule: 2, "non applicable si" : "top . sum . evt . ko", espace: "top"},
{nom: "evt", espace: "top . sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"},
{nom: "ko", espace: "top . sum . evt"}],
rules = rawRules.map(enrichRule),
situation = analyseSituation(rules,"sum")(stateSelector),
result = buildNextSteps(rules, situation)
expect(result).to.have.lengthOf(1)
expect(R.path(["question","props","label"])(result[0])).to.equal("?")
});
});

20
test/helpers/browser.js Normal file
View File

@ -0,0 +1,20 @@
require('babel-register')();
var jsdom = require('jsdom/lib/old-api').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js'
};
documentRef = document;

564
test/helpers/runner.js Normal file
View File

@ -0,0 +1,564 @@
const noop = () => {}
const loadYaml = (module, filename) => {
const yaml = require('js-yaml');
module.exports = yaml.safeLoad(fs.readFileSync(filename, 'utf8'));
}
const loadNearley = (module, filename) => {
var nearley = require('nearley/lib/nearley.js');
var compile = require('nearley/lib/compile.js');
var generate = require('nearley/lib/generate.js');
var grammar = require('nearley/lib/nearley-language-bootstrapped.js');
var parser = new nearley.Parser(grammar.ParserRules, grammar.ParserStart);
parser.feed(fs.readFileSync(filename, 'utf8'));
var compilation = compile(parser.results[0], {});
var content = generate(compilation, 'Grammar');
module._compile(content,filename)
}
require.extensions['.yaml'] = loadYaml
require.extensions['.ne'] = loadNearley
require.extensions['.css'] = noop
/**
* Module dependencies.
*/
var program = require('commander');
var path = require('path');
var fs = require('fs');
var resolve = path.resolve;
var exists = fs.existsSync || path.existsSync;
var Mocha = require('mocha');
var utils = Mocha.utils;
var interfaceNames = Object.keys(Mocha.interfaces);
var join = path.join;
var cwd = process.cwd();
var getOptions = require('mocha/bin/options');
var mocha = new Mocha();
/**
* Save timer references to avoid Sinon interfering (see GH-237).
*/
var Date = global.Date;
var setTimeout = global.setTimeout;
var setInterval = global.setInterval;
var clearTimeout = global.clearTimeout;
var clearInterval = global.clearInterval;
/**
* Files.
*/
var files = [];
/**
* Globals.
*/
var globals = [];
/**
* Requires.
*/
var requires = [];
// options
program
.usage('[debug] [options] [files]')
.option('-A, --async-only', 'force all tests to take a callback (async) or return a promise')
.option('-c, --colors', 'force enabling of colors')
.option('-C, --no-colors', 'force disabling of colors')
.option('-G, --growl', 'enable growl notification support')
.option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
.option('-R, --reporter <name>', 'specify the reporter to use', 'spec')
.option('-S, --sort', 'sort test files')
.option('-b, --bail', 'bail after first test failure')
.option('-d, --debug', "enable node's debugger, synonym for node --debug")
.option('-g, --grep <pattern>', 'only run tests matching <pattern>')
.option('-f, --fgrep <string>', 'only run tests containing <string>')
.option('-gc, --expose-gc', 'expose gc extension')
.option('-i, --invert', 'inverts --grep and --fgrep matches')
.option('-r, --require <name>', 'require the given module')
.option('-s, --slow <ms>', '"slow" test threshold in milliseconds [75]')
.option('-t, --timeout <ms>', 'set test-case timeout in milliseconds [2000]')
.option('-u, --ui <name>', 'specify user-interface (' + interfaceNames.join('|') + ')', 'bdd')
.option('-w, --watch', 'watch files for changes')
.option('--check-leaks', 'check for global variable leaks')
.option('--full-trace', 'display the full stack trace')
.option('--compilers <ext>:<module>,...', 'use the given module(s) to compile files', list, [])
.option('--debug-brk', "enable node's debugger breaking on the first line")
.option('--globals <names>', 'allow the given comma-delimited global [names]', list, [])
.option('--es_staging', 'enable all staged features')
.option('--harmony<_classes,_generators,...>', 'all node --harmony* flags are available')
.option('--preserve-symlinks', 'Instructs the module loader to preserve symbolic links when resolving and caching modules')
.option('--icu-data-dir', 'include ICU data')
.option('--inline-diffs', 'display actual/expected differences inline within each string')
.option('--inspect', 'activate devtools in chrome')
.option('--inspect-brk', 'activate devtools in chrome and break on the first line')
.option('--interfaces', 'display available interfaces')
.option('--no-deprecation', 'silence deprecation warnings')
.option('--no-exit', 'require a clean shutdown of the event loop: mocha will not call process.exit')
.option('--no-timeouts', 'disables timeouts, given implicitly with --debug')
.option('--no-warnings', 'silence all node process warnings')
.option('--opts <path>', 'specify opts path', 'test/mocha.opts')
.option('--perf-basic-prof', 'enable perf linux profiler (basic support)')
.option('--napi-modules', 'enable experimental NAPI modules')
.option('--prof', 'log statistical profiling information')
.option('--log-timer-events', 'Time events including external callbacks')
.option('--recursive', 'include sub directories')
.option('--reporters', 'display available reporters')
.option('--retries <times>', 'set numbers of time to retry a failed test case')
.option('--throw-deprecation', 'throw an exception anytime a deprecated function is used')
.option('--trace', 'trace function calls')
.option('--trace-deprecation', 'show stack traces on deprecations')
.option('--trace-warnings', 'show stack traces on node process warnings')
.option('--use_strict', 'enforce strict mode')
.option('--watch-extensions <ext>,...', 'additional extensions to monitor with --watch', list, [])
.option('--delay', 'wait for async suite definition')
.option('--allow-uncaught', 'enable uncaught errors to propagate')
.option('--forbid-only', 'causes test marked with only to fail the suite')
.option('--forbid-pending', 'causes pending tests and test marked with skip to fail the suite');
program._name = 'mocha';
// --globals
program.on('globals', function (val) {
globals = globals.concat(list(val));
});
// --reporters
program.on('reporters', function () {
console.log();
console.log(' dot - dot matrix');
console.log(' doc - html documentation');
console.log(' spec - hierarchical spec list');
console.log(' json - single json object');
console.log(' progress - progress bar');
console.log(' list - spec-style listing');
console.log(' tap - test-anything-protocol');
console.log(' landing - unicode landing strip');
console.log(' xunit - xunit reporter');
console.log(' min - minimal reporter (great with --watch)');
console.log(' json-stream - newline delimited json events');
console.log(' markdown - markdown documentation (github flavour)');
console.log(' nyan - nyan cat!');
console.log();
process.exit();
});
// --interfaces
program.on('interfaces', function () {
console.log('');
interfaceNames.forEach(function (interfaceName) {
console.log(' ' + interfaceName);
});
console.log('');
process.exit();
});
// -r, --require
module.paths.push(cwd, join(cwd, 'node_modules'));
program.on('require', function (mod) {
var abs = exists(mod) || exists(mod + '.js');
if (abs) {
mod = resolve(mod);
}
requires.push(mod);
});
// If not already done, load mocha.opts
if (!process.env.LOADED_MOCHA_OPTS) {
getOptions();
}
// parse args
program.parse(process.argv);
// infinite stack traces
Error.stackTraceLimit = Infinity; // TODO: config
// reporter options
var reporterOptions = {};
if (program.reporterOptions !== undefined) {
program.reporterOptions.split(',').forEach(function (opt) {
var L = opt.split('=');
if (L.length > 2 || L.length === 0) {
throw new Error("invalid reporter option '" + opt + "'");
} else if (L.length === 2) {
reporterOptions[L[0]] = L[1];
} else {
reporterOptions[L[0]] = true;
}
});
}
// reporter
mocha.reporter(program.reporter, reporterOptions);
// load reporter
var Reporter = null;
try {
Reporter = require('mocha/lib/reporters/' + program.reporter);
} catch (err) {
try {
Reporter = require(program.reporter);
} catch (err2) {
throw new Error('reporter "' + program.reporter + '" does not exist');
}
}
// --no-colors
if (!program.colors) {
mocha.useColors(false);
}
// --colors
if (~process.argv.indexOf('--colors') || ~process.argv.indexOf('-c')) {
mocha.useColors(true);
}
// --inline-diffs
if (program.inlineDiffs) {
mocha.useInlineDiffs(true);
}
// --slow <ms>
if (program.slow) {
mocha.suite.slow(program.slow);
}
// --no-timeouts
if (!program.timeouts) {
mocha.enableTimeouts(false);
}
// --timeout
if (program.timeout) {
mocha.suite.timeout(program.timeout);
}
// --bail
mocha.suite.bail(program.bail);
// --grep
if (program.grep) {
mocha.grep(program.grep);
}
// --fgrep
if (program.fgrep) {
mocha.fgrep(program.fgrep);
}
// --invert
if (program.invert) {
mocha.invert();
}
// --check-leaks
if (program.checkLeaks) {
mocha.checkLeaks();
}
// --stack-trace
if (program.fullTrace) {
mocha.fullTrace();
}
// --growl
if (program.growl) {
mocha.growl();
}
// --async-only
if (program.asyncOnly) {
mocha.asyncOnly();
}
// --delay
if (program.delay) {
mocha.delay();
}
// --allow-uncaught
if (program.allowUncaught) {
mocha.allowUncaught();
}
// --globals
mocha.globals(globals);
// --retries
if (program.retries) {
mocha.suite.retries(program.retries);
}
// --forbid-only
if (program.forbidOnly) mocha.forbidOnly();
// --forbid-pending
if (program.forbidPending) mocha.forbidPending();
// custom compiler support
var extensions = ['js'];
program.compilers.forEach(function (c) {
var idx = c.indexOf(':');
var ext = c.slice(0, idx);
var mod = c.slice(idx + 1);
if (mod[0] === '.') {
mod = join(process.cwd(), mod);
}
require(mod);
extensions.push(ext);
program.watchExtensions.push(ext);
});
// requires
requires.forEach(function (mod) {
require(mod);
});
// interface
mocha.ui(program.ui);
// args
var args = program.args;
// default files to test/*.{js,coffee}
if (!args.length) {
args.push('test');
}
args.forEach(function (arg) {
var newFiles;
try {
newFiles = utils.lookupFiles(arg, extensions, program.recursive);
} catch (err) {
if (err.message.indexOf('cannot resolve path') === 0) {
console.error('Warning: Could not find any test files matching pattern: ' + arg);
return;
}
throw err;
}
files = files.concat(newFiles);
});
if (!files.length) {
console.error('No test files found');
process.exit(1);
}
// resolve
files = files.map(function (path) {
return resolve(path);
});
if (program.sort) {
files.sort();
}
// --watch
var runner;
var loadAndRun;
var purge;
var rerun;
if (program.watch) {
console.log();
hideCursor();
process.on('SIGINT', function () {
showCursor();
console.log('\n');
process.exit(130);
});
var watchFiles = utils.files(cwd, [ 'js' ].concat(program.watchExtensions));
var runAgain = false;
loadAndRun = function loadAndRun () {
try {
mocha.files = files;
runAgain = false;
runner = mocha.run(function () {
runner = null;
if (runAgain) {
rerun();
}
});
} catch (e) {
console.log(e.stack);
}
};
purge = function purge () {
watchFiles.forEach(function (file) {
delete require.cache[file];
});
};
loadAndRun();
rerun = function rerun () {
purge();
stop();
if (!program.grep) {
mocha.grep(null);
}
mocha.suite = mocha.suite.clone();
mocha.suite.ctx = new Mocha.Context();
mocha.ui(program.ui);
loadAndRun();
};
utils.watch(watchFiles, function () {
runAgain = true;
if (runner) {
runner.abort();
} else {
rerun();
}
});
} else {
// load
mocha.files = files;
runner = mocha.run(program.exit ? exit : exitLater);
}
function exitLater (code) {
process.on('exit', function () {
process.exit(Math.min(code, 255));
});
}
function exit (code) {
var clampedCode = Math.min(code, 255);
// Eagerly set the process's exit code in case stream.write doesn't
// execute its callback before the process terminates.
process.exitCode = clampedCode;
// flush output for Node.js Windows pipe bug
// https://github.com/joyent/node/issues/6247 is just one bug example
// https://github.com/visionmedia/mocha/issues/333 has a good discussion
function done () {
if (!(draining--)) {
process.exit(clampedCode);
}
}
var draining = 0;
var streams = [process.stdout, process.stderr];
streams.forEach(function (stream) {
// submit empty write request and wait for completion
draining += 1;
stream.write('', done);
});
done();
}
process.on('SIGINT', function () {
runner.abort();
// This is a hack:
// Instead of `process.exit(130)`, set runner.failures to 130 (exit code for SIGINT)
// The amount of failures will be emitted as error code later
runner.failures = 130;
});
/**
* Parse list.
*/
function list (str) {
return str.split(/ *, */);
}
/**
* Hide the cursor.
*/
function hideCursor () {
process.stdout.write('\u001b[?25l');
}
/**
* Show the cursor.
*/
function showCursor () {
process.stdout.write('\u001b[?25h');
}
/**
* Stop play()ing.
*/
function stop () {
process.stdout.write('\u001b[2K');
clearInterval(play.timer);
}
/**
* Play the given array of strings.
*/
function play (arr, interval) {
var len = arr.length;
interval = interval || 100;
var i = 0;
play.timer = setInterval(function () {
var str = arr[i++ % len];
process.stdout.write('\u001b[0G' + str);
}, interval);
}

2
test/mocha.opts Normal file
View File

@ -0,0 +1,2 @@
test/**/*.test.js

54
test/rules.test.js Normal file
View File

@ -0,0 +1,54 @@
import R from 'ramda'
import {expect} from 'chai'
import {rules, enrichRule, findVariantsAndRecords} from '../source/engine/rules'
import {analyseSituation} from '../source/engine/traverse'
let stateSelector = (state, name) => null
describe('enrichRule', function() {
it('should extract the type of the rule', function() {
let rule = {cotisation:{}}
expect(enrichRule(rule)).to.have.property('type','cotisation')
});
it('should extract the dotted name of the rule', function() {
let rule = {espace:"contrat salarié", nom: "CDD"}
expect(enrichRule(rule)).to.have.property('name','CDD')
expect(enrichRule(rule)).to.have.property('dottedName','contrat salarié . CDD')
});
it('should render Markdown in sub-questions', function() {
let rule = {"sous-question":"**wut**"}
expect(enrichRule(rule)).to.have.property('subquestion','<p><strong>wut</strong></p>\n')
});
});
describe('findVariantsAndRecords', function() {
it('should classify rules as records by default', function() {
let rawRules = [
{nom: "startHere", formule: {somme: [3259, "dix"]}, espace: "top"},
{nom: "dix", formule: "cinq", espace: "top"},
{nom: "cinq", espace: "top", question:"?"}],
rules = rawRules.map(enrichRule),
situation = analyseSituation(rules,"startHere")(stateSelector),
result = findVariantsAndRecords(rules, ['top . cinq'])
expect(result).to.have.deep.property('recordGroups', {top: ['top . cinq']})
});
it('should classify rules as variants if they are named in a "one of these" formula', function() {
let rawRules = [
{nom: "sum", formule: {somme: [2, "deux"]}, espace: "top"},
{nom: "deux", formule: 2, "non applicable si" : "top . sum . evt . ko", espace: "top"},
{nom: "evt", espace: "top . sum", formule: {"une possibilité":["ko"]}, titre: "Truc", question:"?"},
{nom: "ko", espace: "top . sum . evt"}],
rules = rawRules.map(enrichRule),
situation = analyseSituation(rules,"sum")(stateSelector),
result = findVariantsAndRecords(rules, ['top . sum . evt . ko'])
expect(result).to.have.deep.property('variantGroups', {"top . sum . evt": ['top . sum . evt . ko']})
});
});

View File

@ -56,6 +56,24 @@ describe('analyseSituation on raw rules', function() {
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3259)
});
it('should handle complements', function() {
let rawRules = [
{nom: "startHere", formule: {complément: {cible: "dix", montant: 93}}, espace: "top"},
{nom: "dix", formule: 17, espace: "top"}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',93-17)
});
it('should handle components in complements', function() {
let rawRules = [
{nom: "startHere", formule: {complément: {cible: "dix",
composantes: [{montant: 93},{montant: 93}]
}}, espace: "top"},
{nom: "dix", formule: 17, espace: "top"}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',2*(93-17))
});
/* TODO: make this pass
it('should handle applicability conditions', function() {
let rawRules = [
@ -118,6 +136,24 @@ describe('analyseSituation with mecanisms', function() {
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',4800)
});
it('should handle components in multiplication', function() {
let rawRules = [
{nom: "startHere", formule: {"multiplication": {assiette:3200,
composantes: [{taux:0.7}, {taux:0.8}]
}}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',4800)
});
it('should apply a ceiling to the sum of components', function() {
let rawRules = [
{nom: "startHere", formule: {"multiplication": {assiette:3259, plafond:3200,
composantes: [{taux:0.7}, {taux:0.8}]
}}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',4800)
});
it('should handle progressive scales', function() {
let rawRules = [
{nom: "startHere", formule: {"barème": {
@ -129,6 +165,20 @@ describe('analyseSituation with mecanisms', function() {
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',100+1200+80)
});
it('should handle progressive scales with components', function() {
let rawRules = [
{nom: "startHere", formule: {"barème": {
assiette:2008,
"multiplicateur des tranches":1000,
composantes: [
{"tranches":[{"en-dessous de":1, taux: 0.05},{de:1, "à": 2, taux: 0.4}, ,{"au-dessus de":2, taux: 5}]},
{"tranches":[{"en-dessous de":1, taux: 0.05},{de:1, "à": 2, taux: 0.8}, ,{"au-dessus de":2, taux: 5}]}
]
}}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',100+1200+80)
});
it('should handle max', function() {
let rawRules = [
{nom: "startHere", formule: {"le maximum de": [3200, 60, 9]}}],
@ -136,4 +186,23 @@ describe('analyseSituation with mecanisms', function() {
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',3200)
});
it('should handle filtering on components', function() {
let rawRules = [
{nom: "startHere", espace: "top", formule: "composed (salarié)"},
{nom: "composed", espace: "top", formule: {"barème": {
assiette:2008,
"multiplicateur des tranches":1000,
composantes: [
{tranches:[{"en-dessous de":1, taux: 0.05},{de:1, "à": 2, taux: 0.4}, ,{"au-dessus de":2, taux: 5}],
attributs: {"dû par":"salarié"}
},
{tranches:[{"en-dessous de":1, taux: 0.05},{de:1, "à": 2, taux: 0.8}, ,{"au-dessus de":2, taux: 5}],
attributs: {"dû par":"employeur"}
}
]
}}}],
rules = rawRules.map(enrichRule)
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',50+400+40)
});
});

54
test/variables.test.js Normal file
View File

@ -0,0 +1,54 @@
import {expect} from 'chai'
import {evaluateBottomUp, evaluateVariable} from '../source/engine/variables'
describe('evaluateVariable', function() {
it ("should directly return the value of any rule that specifies a format (i.e currency, duration)", function() {
let rule = {format: "euros"},
state = {salaire: "2300"},
situationGate = (name) => state[name]
expect(evaluateVariable(situationGate, "salaire", rule)).to.equal("2300")
});
it ("should interpret rules without a formula as boolean-valued, with 'oui' for true", function() {
let rule = {},
state = {condition: "oui"},
situationGate = (name) => state[name]
expect(evaluateVariable(situationGate, "condition", rule)).to.be.true
});
it ("should interpret rules without a formula as boolean-valued, with values other than 'oui' meaning false", function() {
let rule = {},
state = {condition: "nope"},
situationGate = (name) => state[name]
expect(evaluateVariable(situationGate, "condition", rule)).to.be.false
});
it ("should interpret rules with 'one of these', with 'oui' for true", function() {
let rule = {formule: {"une possibilité": ["noir","blanc"]}},
state = {condition: "oui"},
situationGate = (name) => state[name]
expect(evaluateVariable(situationGate, "condition", rule)).to.be.true
});
it ("should walk up the namespace chain until it finds the tail as the value", function() {
let rule = {formule: {"une possibilité": ["noir","blanc"]}},
state = {"contrat salarié . CDD . motif": "classique . accroissement activité"},
situationGate = (name) => state[name]
expect(evaluateVariable(situationGate, "contrat salarié . CDD . motif . classique . accroissement activité", rule)).to.be.true
});
it ("should return null if a value isn't found for the name given", function() {
let rule = {formule: {"une possibilité": ["noir","blanc"]}},
state = {"condition": "classique . accroissement activité"},
situationGate = (name) => state[name]
expect(evaluateVariable(situationGate, "contrat salarié . CDD . motif . classique . accroissement activité", rule)).to.be.null
});
});