diff --git a/.gitignore b/.gitignore
index 4b111963a..9cb928213 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
.tmp
node_modules/
dist/
+.DS_Store
diff --git a/__tests__/rules.test.js b/__tests__/rules.test.js
deleted file mode 100644
index 796ea56ec..000000000
--- a/__tests__/rules.test.js
+++ /dev/null
@@ -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','
wut
\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')
- });
-
-});
diff --git a/package.json b/package.json
index cf3e278a9..f7a840f15 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/règles/rémunération-travail/cdd/majoration-chomage.yaml b/règles/rémunération-travail/cdd/majoration-chomage.yaml
index b7d2f62c3..b7f702127 100644
--- a/règles/rémunération-travail/cdd/majoration-chomage.yaml
+++ b/règles/rémunération-travail/cdd/majoration-chomage.yaml
@@ -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: |
diff --git a/règles/rémunération-travail/cotisations/agff.yaml b/règles/rémunération-travail/cotisations/agff.yaml
deleted file mode 100644
index 9306315a2..000000000
--- a/règles/rémunération-travail/cotisations/agff.yaml
+++ /dev/null
@@ -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%
diff --git a/règles/rémunération-travail/cotisations/maladie.yaml b/règles/rémunération-travail/cotisations/maladie.yaml
deleted file mode 100644
index b2a6fd7d3..000000000
--- a/règles/rémunération-travail/cotisations/maladie.yaml
+++ /dev/null
@@ -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
diff --git a/règles/rémunération-travail/cotisations/ok/agff.yaml b/règles/rémunération-travail/cotisations/ok/agff.yaml
new file mode 100644
index 000000000..bc4159d18
--- /dev/null
+++ b/règles/rémunération-travail/cotisations/ok/agff.yaml
@@ -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%
diff --git a/règles/rémunération-travail/cotisations/agirc-gmp.yaml b/règles/rémunération-travail/cotisations/ok/agirc-gmp.yaml
similarity index 52%
rename from règles/rémunération-travail/cotisations/agirc-gmp.yaml
rename to règles/rémunération-travail/cotisations/ok/agirc-gmp.yaml
index 779f3d7cd..46beaaa55 100644
--- a/règles/rémunération-travail/cotisations/agirc-gmp.yaml
+++ b/règles/rémunération-travail/cotisations/ok/agirc-gmp.yaml
@@ -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
diff --git a/règles/rémunération-travail/cotisations/ok/agirc.yaml b/règles/rémunération-travail/cotisations/ok/agirc.yaml
index aadc73c7f..e9c1f1f6c 100644
--- a/règles/rémunération-travail/cotisations/ok/agirc.yaml
+++ b/règles/rémunération-travail/cotisations/ok/agirc.yaml
@@ -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:
diff --git a/règles/rémunération-travail/cotisations/ok/maladie.yaml b/règles/rémunération-travail/cotisations/ok/maladie.yaml
new file mode 100644
index 000000000..650c2ca9d
--- /dev/null
+++ b/règles/rémunération-travail/cotisations/ok/maladie.yaml
@@ -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%
diff --git a/règles/rémunération-travail/cotisations/ok/vieillesse.yaml b/règles/rémunération-travail/cotisations/ok/vieillesse.yaml
new file mode 100644
index 000000000..363daf2c9
--- /dev/null
+++ b/règles/rémunération-travail/cotisations/ok/vieillesse.yaml
@@ -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%
diff --git a/règles/rémunération-travail/cotisations/vieillesse.yaml b/règles/rémunération-travail/cotisations/vieillesse.yaml
deleted file mode 100644
index 76e50a495..000000000
--- a/règles/rémunération-travail/cotisations/vieillesse.yaml
+++ /dev/null
@@ -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%
diff --git a/règles/rémunération-travail/entités/ok/contrat-salarié.yaml b/règles/rémunération-travail/entités/ok/contrat-salarié.yaml
index 8888d2d10..ab7723479 100644
--- a/règles/rémunération-travail/entités/ok/contrat-salarié.yaml
+++ b/règles/rémunération-travail/entités/ok/contrat-salarié.yaml
@@ -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
diff --git a/source/.babelrc b/source/.babelrc
index 8353486b5..b310a8754 100644
--- a/source/.babelrc
+++ b/source/.babelrc
@@ -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" }]
]
}
diff --git a/source/components/Aide.js b/source/components/Aide.js
index 4137919a3..77f1bf868 100644
--- a/source/components/Aide.js
+++ b/source/components/Aide.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 =>
diff --git a/source/components/AttachDictionary.js b/source/components/AttachDictionary.js
index bb18d089c..a11222b97 100644
--- a/source/components/AttachDictionary.js
+++ b/source/components/AttachDictionary.js
@@ -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
diff --git a/source/components/HomeSyso.js b/source/components/HomeSyso.js
index 4cca32748..9b031a146 100644
--- a/source/components/HomeSyso.js
+++ b/source/components/HomeSyso.js
@@ -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 = {
diff --git a/source/components/Results.js b/source/components/Results.js
index 1ed9c3f60..5e71567ae 100644
--- a/source/components/Results.js
+++ b/source/components/Results.js
@@ -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))
diff --git a/source/components/Satisfaction.js b/source/components/Satisfaction.js
index 61099a72f..f6c586d2f 100644
--- a/source/components/Satisfaction.js
+++ b/source/components/Satisfaction.js
@@ -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'
diff --git a/source/components/Simulateur.js b/source/components/Simulateur.js
index 286e6bc9e..030570f23 100644
--- a/source/components/Simulateur.js
+++ b/source/components/Simulateur.js
@@ -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')
diff --git a/source/components/conversation/Explicable.js b/source/components/conversation/Explicable.js
index 07d9180c8..7e595978b 100644
--- a/source/components/conversation/Explicable.js
+++ b/source/components/conversation/Explicable.js
@@ -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 => ({
diff --git a/source/components/rule/References.js b/source/components/rule/References.js
index b71451418..c71b68800 100644
--- a/source/components/rule/References.js
+++ b/source/components/rule/References.js
@@ -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}) => (
diff --git a/source/engine/generateQuestions.js b/source/engine/generateQuestions.js
index 0b6eac9ca..42a653926 100644
--- a/source/engine/generateQuestions.js
+++ b/source/engine/generateQuestions.js
@@ -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),
diff --git a/source/engine/grammar.ne b/source/engine/grammar.ne
index c2ad31e9e..362600b19 100644
--- a/source/engine/grammar.ne
+++ b/source/engine/grammar.ne
@@ -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 %}
diff --git a/source/engine/known-mecanisms.yaml b/source/engine/known-mecanisms.yaml
index a8e0a0411..52605eb08 100644
--- a/source/engine/known-mecanisms.yaml
+++ b/source/engine/known-mecanisms.yaml
@@ -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: |
diff --git a/source/engine/load-rules.js b/source/engine/load-rules.js
index 9360b4ddd..57af2a535 100644
--- a/source/engine/load-rules.js
+++ b/source/engine/load-rules.js
@@ -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
diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js
index 926e39b31..d56863d5c 100644
--- a/source/engine/mecanisms.js
+++ b/source/engine/mecanisms.js
@@ -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:
+ { composantes.map((c, i) =>
+ [-
+
+ {R.toPairs(c.composante).map(([k,v]) =>
+ -
+ {k}:
+ {v}
+
+ )}
+
+
+ {c.jsx}
+
+ ,
+ i < (composantes.length - 1) &&
+ ]
+ )
+ }
+
+ }
+ />
+ }
+}
+
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:
- { composantes.map((c, i) =>
- [
-
- {R.toPairs(c.composante).map(([k,v]) =>
- -
- {k}:
- {v}
-
- )}
-
-
- {c.jsx}
-
- ,
- i < (composantes.length - 1) &&
- ]
- )
- }
-
- }
- />
- }
}
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:
+
+ montant calculé:
+ {cible.jsx}
+
+
+ montant à atteindre:
+ {mini.jsx}
+
+
+ }
+ />
+ }
+}
+
export let mecanismError = (recurse,k,v) => {
throw "Le mécanisme est inconnu !"
}
diff --git a/source/engine/rules.js b/source/engine/rules.js
index 8b1afe1ab..ddc212f71 100644
--- a/source/engine/rules.js
+++ b/source/engine/rules.js
@@ -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]}),
diff --git a/source/engine/traverse-common-jsx.js b/source/engine/traverse-common-jsx.js
index b4cdcdda5..bc79be27b 100644
--- a/source/engine/traverse-common-jsx.js
+++ b/source/engine/traverse-common-jsx.js
@@ -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}) => (
{name &&
- {name}
+
+ {name}
+
}
)
diff --git a/source/engine/traverse.js b/source/engine/traverse.js
index 12c2e60a6..1191281fb 100644
--- a/source/engine/traverse.js
+++ b/source/engine/traverse.js
@@ -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',
{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)
diff --git a/source/reducers.js b/source/reducers.js
index 902bf4144..58d9e3ca7 100644
--- a/source/reducers.js
+++ b/source/reducers.js
@@ -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,
diff --git a/test/generateQuestions.test.js b/test/generateQuestions.test.js
new file mode 100644
index 000000000..50858b2cf
--- /dev/null
+++ b/test/generateQuestions.test.js
@@ -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("?")
+ });
+
+});
diff --git a/test/helpers/browser.js b/test/helpers/browser.js
new file mode 100644
index 000000000..b7c9d4b2a
--- /dev/null
+++ b/test/helpers/browser.js
@@ -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;
diff --git a/test/helpers/runner.js b/test/helpers/runner.js
new file mode 100644
index 000000000..129b04a38
--- /dev/null
+++ b/test/helpers/runner.js
@@ -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 ', 'reporter-specific options')
+ .option('-R, --reporter ', '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 ', 'only run tests matching ')
+ .option('-f, --fgrep ', 'only run tests containing ')
+ .option('-gc, --expose-gc', 'expose gc extension')
+ .option('-i, --invert', 'inverts --grep and --fgrep matches')
+ .option('-r, --require ', 'require the given module')
+ .option('-s, --slow ', '"slow" test threshold in milliseconds [75]')
+ .option('-t, --timeout ', 'set test-case timeout in milliseconds [2000]')
+ .option('-u, --ui ', '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 :,...', 'use the given module(s) to compile files', list, [])
+ .option('--debug-brk', "enable node's debugger breaking on the first line")
+ .option('--globals ', '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 ', '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 ', '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 ,...', '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
+
+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);
+}
diff --git a/test/mocha.opts b/test/mocha.opts
new file mode 100644
index 000000000..880f37f14
--- /dev/null
+++ b/test/mocha.opts
@@ -0,0 +1,2 @@
+test/**/*.test.js
+
diff --git a/test/rules.test.js b/test/rules.test.js
new file mode 100644
index 000000000..deb6a9187
--- /dev/null
+++ b/test/rules.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','wut
\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']})
+ });
+
+});
diff --git a/__tests__/traverse.test.js b/test/traverse.test.js
similarity index 63%
rename from __tests__/traverse.test.js
rename to test/traverse.test.js
index b718fd712..0951c0e41 100644
--- a/__tests__/traverse.test.js
+++ b/test/traverse.test.js
@@ -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)
+ });
+
});
diff --git a/__tests__/utils.test.js b/test/utils.test.js
similarity index 100%
rename from __tests__/utils.test.js
rename to test/utils.test.js
diff --git a/test/variables.test.js b/test/variables.test.js
new file mode 100644
index 000000000..688f294f5
--- /dev/null
+++ b/test/variables.test.js
@@ -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
+ });
+
+});