Merge pull request #15 from sgmap/new-cotisations
Calcule le salaire net et le cout du travail à partir du brutpull/44/head
commit
709a15cec4
|
@ -2,3 +2,4 @@
|
|||
.tmp
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
|
|
|
@ -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')
|
||||
});
|
||||
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: |
|
||||
|
|
|
@ -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 l’AGIRC et de l’ARRCO)
|
||||
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%
|
|
@ -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
|
|
@ -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 l’AGIRC et de l’ARRCO)
|
||||
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%
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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%
|
|
@ -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%
|
|
@ -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%
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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" }]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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: |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 !"
|
||||
}
|
||||
|
|
|
@ -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]}),
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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("?")
|
||||
});
|
||||
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
test/**/*.test.js
|
||||
|
|
@ -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']})
|
||||
});
|
||||
|
||||
});
|
|
@ -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)
|
||||
});
|
||||
|
||||
});
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in New Issue