diff --git a/publicode/README.md b/publicode/README.md index 0834e5ebd..f58eede1d 100644 --- a/publicode/README.md +++ b/publicode/README.md @@ -144,19 +144,15 @@ impôt sur le revenu: barème: assiette: revenu imposable tranches: - - en-dessous de: 9807 - taux: 0% - - de: 9807 - à: 27086 - taux: 14% - - de: 27086 - à: 72617 - taux: 30% - - de: 72617 - à: 153783 - taux: 41% - - au-dessus de: 153783 - taux: 45% + - taux: 0% + plafond: 9807 + - taux: 14% + plafond: 27086 + - taux: 30% + plafond: 72617 + - taux: 41% + plafond: 153783 + - taux: 45% ``` La syntaxe hiérarchique de Yaml permet d'imbriquer les mécanismes : diff --git a/publicode/rules/artiste-auteur.yaml b/publicode/rules/artiste-auteur.yaml index 2a0299c22..609459b41 100644 --- a/publicode/rules/artiste-auteur.yaml +++ b/publicode/rules/artiste-auteur.yaml @@ -95,8 +95,8 @@ artiste-auteur . cotisations . vieillesse: assiette: assiette composantes: - nom: plafonnée - plafond: contrat salarié . plafond sécurité sociale taux: contrat salarié . vieillesse . taux salarié plafonné - 0.75% + plafond: contrat salarié . plafond sécurité sociale - nom: non plafonnée taux: contrat salarié . vieillesse . taux salarié non plafonné - 0.4% @@ -116,8 +116,8 @@ artiste-auteur . cotisations . CSG-CRDS . abattement: formule: multiplication: assiette: revenus . traitements et salaires - plafond: 4 * contrat salarié . plafond sécurité sociale taux: 1.75% + plafond: 4 * contrat salarié . plafond sécurité sociale artiste-auteur . cotisations . CSG-CRDS . CSG: formule: diff --git a/publicode/rules/conventions-collectives.yaml b/publicode/rules/conventions-collectives.yaml index 2d657c104..665a6337a 100644 --- a/publicode/rules/conventions-collectives.yaml +++ b/publicode/rules/conventions-collectives.yaml @@ -39,15 +39,13 @@ contrat salarié . convention collective . HCR . majoration heures supplémentai formule: barème: assiette: temps de travail . heures supplémentaires - multiplicateur: 1 heure/semaine * période . semaines par mois + multiplicateur: période . semaines par mois tranches: - - en-dessous de: 4 - taux: 10% - - de: 4 - à: 8 - taux: 20% - - au-dessus de: 8 - taux: 50% + - taux: 10% + plafond: 4 heures/semaine + - taux: 20% + plafond: 8 heures/semaine + - taux: 50% contrat salarié . convention collective . SVP: titre: Spectacle vivant privé (beta) @@ -298,28 +296,21 @@ contrat salarié . convention collective . sport . cotisations . assiette forfai applicable si: assiette franchisée < SMIC horaire * 115 heures/mois remplace: contrat salarié . cotisations . assiette forfaitaire formule: - multiplication: - assiette: SMIC horaire - facteur: - barème linéaire: - assiette: assiette franchisée [€/mois] - multiplicateur: SMIC horaire - unité: heures/mois - tranches: - - en-dessous de: 45 - montant: 5 - - de: 45 - à: 60 - montant: 15 - - de: 60 - à: 80 - montant: 25 - - de: 80 - à: 100 - montant: 35 - - de: 100 - à: 115 - montant: 50 + grille: + assiette: assiette franchisée [€/mois] + multiplicateur: SMIC horaire / 1 mois + unité: €/mois + tranches: + - montant: 5 * SMIC horaire + plafond: 45 heures + - montant: 15 * SMIC horaire + plafond: 60 heures + - montant: 25 * SMIC horaire + plafond: 80 heures + - montant: 35 * SMIC horaire + plafond: 100 heures + - montant: 50 * SMIC horaire + plafond: 115 heures contrat salarié . convention collective . sport . primes . nombre de manifestations: question: Combien de manifestations rémunérées le joueur a-t'il effectué ? diff --git a/publicode/rules/dirigeant.yaml b/publicode/rules/dirigeant.yaml index 16f5b8098..05ce2099b 100644 --- a/publicode/rules/dirigeant.yaml +++ b/publicode/rules/dirigeant.yaml @@ -182,10 +182,9 @@ dirigeant . auto-entrepreneur . cotisations et contributions . cotisations: assiette: base des cotisations multiplicateur: plafond ACRE tranches: - - en-dessous de: 1 - taux: taux de cotisation * (100% - taux ACRE) - - au-dessus de: 1 - taux: taux de cotisation + - taux: taux de cotisation * (100% - taux ACRE) + plafond: 1 + - taux: taux de cotisation références: La protection sociale du micro-entrepreneur: https://bpifrance-creation.fr/encyclopedie/micro-entreprise-regime-auto-entrepreneur/fiscal-social-comptable/protection-sociale @@ -409,14 +408,14 @@ dirigeant . indépendant . cotisations et contributions . exonérations . ACRE . dirigeant . indépendant . cotisations et contributions . exonérations . ACRE . taux: formule: - barème continu: - assiette: revenu professionnel + taux progressif: + assiette: assiette des cotisations multiplicateur: PSS proratisé - points: - 0: 100% - 0.75: 100% - 1: 0% - retourne seulement le taux: oui + tranches: + - taux: 100% + plafond: 75% + - taux: 0% + plafond: 100% dirigeant . indépendant . revenu net de cotisations: formule: @@ -430,16 +429,7 @@ dirigeant . indépendant . revenu net de cotisations: unité par défaut: €/an dirigeant . indépendant . revenu professionnel: - titre: revenu professionnel (net imposable) unité par défaut: €/an - description: | - C'est le revenu net de cotisations déductibles du travailleur indépendant, qui sert de base au calcul des cotisations et de l'impôt pour les indépendants. - - Attention, **notre calcul est fait au régime de croisière**: - l'indépendant qui se lance paiera pendant ses 2 premières années un forfait relativement réduit de cotisations sociales. Il devra ensuite régulariser cette situation par rapport au revenu qu'il a vraiment perçu. - - Il faut donc voir ce calcul comme *le montant qui devra de toute façon être payé* à court terme après 2 ans d'exercice. - formule: inversion numérique: avec: @@ -449,6 +439,13 @@ dirigeant . indépendant . revenu professionnel: - entreprise . chiffre d'affaires - entreprise . chiffre d'affaires minimum +dirigeant . indépendant . assiette des cotisations: + unité par défaut: €/an + formule: + encadrement: + plancher: 0 + valeur: revenu professionnel + dirigeant . indépendant . conjoint collaborateur: question: Avez-vous un conjoint collaborateur ? description: | @@ -488,14 +485,14 @@ dirigeant . indépendant . conjoint collaborateur . assiette . revenu avec parta **Cette option baisse le montant des cotisations à payer pour le gérant, mais elle diminue également ses contreparties sociales (pension de retraite, indemnité décès, etc)** formule: assiette = 'revenu avec partage' remplace: - règle: revenu professionnel - par: revenu professionnel - cotisations . assiette + règle: assiette des cotisations + par: assiette des cotisations - cotisations . assiette dans: - cotisations et contributions . cotisations . retraite de base - cotisations et contributions . cotisations . retraite complémentaire - cotisations et contributions . cotisations . invalidité et décès dirigeant . indépendant . conjoint collaborateur . assiette . revenu sans partage: - description: Le conjoint collaborateur paiera des cotisations sociales calculées sur une base d'un pourcentage du revenu professionnel du gérant de l'entreprise (un tiers ou la moitié). + description: Le conjoint collaborateur paiera des cotisations sociales calculées sur une base d'un pourcentage du assiette des cotisations du gérant de l'entreprise (un tiers ou la moitié). formule: assiette = 'revenu sans partage' dirigeant . indépendant . conjoint collaborateur . assiette . pourcentage: @@ -520,7 +517,7 @@ dirigeant . indépendant . conjoint collaborateur . cotisations . assiette: titre: assiette conjoint collaborateur formule: multiplication: - assiette: revenu professionnel + assiette: assiette des cotisations taux: 1 / 3 variations: - si: assiette . forfaitaire @@ -553,21 +550,19 @@ dirigeant . indépendant . conjoint collaborateur . cotisations . retraite de ba assiette: assiette retraite multiplicateur: plafond sécurité sociale temps plein tranches: - - en-dessous de: 1 - taux: 17.75% - - au-dessus de: 1 - taux: 0.6% + - taux: 17.75% + plafond: 1 + - taux: 0.6% dirigeant . indépendant . conjoint collaborateur . cotisations . retraite complémentaire: formule: barème: - assiette: retraite complémentaire . assiette [€/an] + assiette: retraite complémentaire . assiette tranches: - - en-dessous de: 37960 - taux: 7% - - de: 37960 - à: 162096 - taux: 8% + - taux: 7% + plafond: cotisations et contributions . cotisations . retraite complémentaire . plafond + - taux: 8% + plafond: 4 * plafond sécurité sociale temps plein dirigeant . indépendant . conjoint collaborateur . cotisations . retraite complémentaire . assiette: titre: assiette retraite complémentaire @@ -589,8 +584,8 @@ dirigeant . indépendant . conjoint collaborateur . cotisations . invalidité et formule: multiplication: assiette: assiette - plafond: plafond sécurité sociale temps plein taux: 1.3% + plafond: plafond sécurité sociale temps plein dirigeant . indépendant . conjoint collaborateur . cotisations . indemnités journalières maladie: formule: @@ -634,16 +629,19 @@ dirigeant . indépendant . cotisations et contributions . cotisations . déducti par défaut: 0 dirigeant . indépendant . cotisations et contributions . cotisations . déduction tabac . revenus déduits: - titre: revenu professionnel (avec déduction tabac) + titre: assiette des cotisations (avec déduction tabac) applicable si: déduction tabac remplace: - règle: revenu professionnel + règle: assiette des cotisations dans: - retraite de base - retraite complémentaire - invalidité et décès - conjoint collaborateur - formule: revenu professionnel - déduction tabac + formule: + allègement: + assiette: assiette des cotisations + abattement: déduction tabac dirigeant . rattachement CIPAV: description: | @@ -686,12 +684,17 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie remplace: maladie rend non applicable: indemnités journalières maladie formule: - barème continu: - assiette: indépendant . revenu professionnel - multiplicateur: plafond sécurité sociale temps plein - points: - 0: 1.5% - 1.1: 6.5% + multiplication: + assiette: indépendant . assiette des cotisations + taux: + taux progressif: + assiette: indépendant . assiette des cotisations + multiplicateur: plafond sécurité sociale temps plein + tranches: + - plafond: 0% + taux: 1.5% + - plafond: 110% + taux: 6.5% références: secu-independants.fr: https://www.secu-independants.fr/cotisations/calcul-des-cotisations/taux-de-cotisations guide urssaf (pdf): https://www.urssaf.fr/portail/files/live/sites/urssaf/files/documents/Guide-Professions-liberales.pdf @@ -702,11 +705,11 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie formule: variations: - si: situation personnelle . RSA - alors: revenu professionnel + alors: assiette des cotisations - sinon: encadrement: plancher: 40% * plafond sécurité sociale temps plein - valeur: revenu professionnel + valeur: assiette des cotisations références: secu-independants.fr: https://www.secu-independants.fr/cotisations/calcul-des-cotisations/cotisations-minimales/ @@ -725,10 +728,9 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie: assiette: assiette multiplicateur: plafond sécurité sociale temps plein tranches: - - en-dessous de: 5 - taux: taux variable - - au-dessus de: 5 - taux: 6.5% + - taux: taux variable + plafond: 5 + - taux: 6.5% références: décret formule de calcul: https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000036342439&categorieLien=id note: | @@ -757,7 +759,7 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie formule: multiplication: assiette: 5% - taux: revenu professionnel / seuil supérieur de réduction + taux: assiette des cotisations / seuil supérieur de réduction dirigeant . indépendant . cotisations et contributions . cotisations . maladie . seuil supérieur de réduction: formule: @@ -767,14 +769,16 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie dirigeant . indépendant . cotisations et contributions . cotisations . maladie . taux: formule: - barème continu: - retourne seulement le taux: oui - assiette: revenu professionnel + taux progressif: + assiette: assiette des cotisations multiplicateur: plafond sécurité sociale temps plein - points: - 0: 0% - 0.4: 3.168% - 1.1: 6.35% + tranches: + - plafond: 0% + taux: 0% + - plafond: 40% + taux: 3.168% + - plafond: 110% + taux: 6.35% note: | Cette cotisation est à la base très simple : la cotisation maladie est de 7.2% jusqu'à 5 fois le plafond de la sécurité sociale, puis 6.5% au-delà. Cependant : - pour son recouvrement, cette cotisation est séparée en 2 : maladie et maladie 2, cette deuxième au taux de 0.85%. Ainsi le montant de 7.2% cité dans le décret est rabaissé à 6.35% dans nos calculs. @@ -791,11 +795,11 @@ dirigeant . rattachement CIPAV . retraite de base: multiplicateur: composantes: - nom: tranche 1 - plafond: plafond sécurité sociale temps plein taux: 8.23% + plafond: plafond sécurité sociale temps plein - nom: tranche 2 - plafond: 5 * plafond sécurité sociale temps plein taux: 1.87% + plafond: 5 * plafond sécurité sociale temps plein dirigeant . indépendant . cotisations et contributions . cotisations . retraite de base: formule: @@ -803,21 +807,20 @@ dirigeant . indépendant . cotisations et contributions . cotisations . retraite assiette: assiette multiplicateur: plafond sécurité sociale temps plein tranches: - - en-dessous de: 1 - taux: 17.75% - - au-dessus de: 1 - taux: 0.6% + - taux: 17.75% + plafond: 1 + - taux: 0.6% dirigeant . indépendant . cotisations et contributions . cotisations . retraite de base . assiette: titre: assiette retraite de base formule: variations: - si: situation personnelle . RSA - alors: revenu professionnel + alors: assiette des cotisations - sinon: encadrement: plancher: 11.5% * plafond sécurité sociale temps plein - valeur: revenu professionnel + valeur: assiette des cotisations références: secu-independants.fr: https://www.secu-independants.fr/cotisations/calcul-des-cotisations/cotisations-minimales/ @@ -825,31 +828,25 @@ dirigeant . indépendant . cotisations et contributions . cotisations . retraite dirigeant . rattachement CIPAV . retraite complémentaire: remplace: indépendant . cotisations et contributions . cotisations . retraite complémentaire formule: - barème linéaire: - assiette: indépendant . revenu professionnel [€/an] + grille: + assiette: indépendant . assiette des cotisations + unité: €/an tranches: - - en-dessous de: 26580 - montant: 1315 - - de: 26581 - à: 49280 - montant: 2630 - - de: 49281 - à: 57850 - montant: 3945 - - de: 57851 - à: 66400 - montant: 6575 - - de: 66401 - à: 83060 - montant: 9205 - - de: 83061 - à: 103180 - montant: 14465 - - de: 103181 - à: 123300 - montant: 15780 - - au-dessus de: 123300 - montant: 17095 + - montant: 1315 + plafond: 26581 €/an + - montant: 2630 + plafond: 49281 €/an + - montant: 3945 + plafond: 57851 €/an + - montant: 6575 + plafond: 66401 €/an + - montant: 9205 + plafond: 83061 €/an + - montant: 14465 + plafond: 103181 €/an + - montant: 15780 + plafond: 123301 €/an + - montant: 17095 dirigeant . indépendant . cotisations et contributions . cotisations . retraite complémentaire . taux spécifique PLNR: applicable si: @@ -869,30 +866,35 @@ dirigeant . indépendant . cotisations et contributions . cotisations . retraite remplace: retraite complémentaire formule: barème: - assiette: revenu professionnel + assiette: assiette des cotisations multiplicateur: plafond sécurité sociale temps plein tranches: - - de: 1 - à: 4 - taux: 14% + - taux: 0% + plafond: 1 + - taux: 14% + plafond: 4 dirigeant . indépendant . cotisations et contributions . cotisations . retraite complémentaire: formule: barème: - assiette: revenu professionnel [€/an] + assiette: assiette des cotisations tranches: - - en-dessous de: 37960 - taux: 7% - - de: 37960 - à: 162096 - taux: 8% + - taux: 7% + plafond: plafond + - taux: 8% + plafond: 4 * plafond sécurité sociale temps plein + +dirigeant . indépendant . cotisations et contributions . cotisations . retraite complémentaire . plafond: + acronyme: PRCI + titre: plafond retraite complémentaire des indépendants + formule: 38340 €/an dirigeant . indépendant . cotisations et contributions . cotisations . invalidité et décès: formule: multiplication: assiette: assiette - plafond: plafond sécurité sociale temps plein taux: 1.3% + plafond: plafond sécurité sociale temps plein # TODO invalidité décès pour les libéraux # 3 classes, 76, 228, 380€ @@ -901,10 +903,10 @@ dirigeant . indépendant . cotisations et contributions . cotisations . invalidi formule: variations: - si: situation personnelle . RSA - alors: revenu professionnel + alors: assiette des cotisations - sinon: encadrement: - valeur: revenu professionnel + valeur: assiette des cotisations plancher: 11.5% * plafond sécurité sociale temps plein références: secu-independants.fr: https://www.secu-independants.fr/cotisations/calcul-des-cotisations/cotisations-minimales/ @@ -980,13 +982,17 @@ dirigeant . indépendant . cotisations et contributions . formation professionne dirigeant . indépendant . cotisations et contributions . cotisations . allocations familiales: formule: - barème continu: - assiette: revenu professionnel - multiplicateur: plafond sécurité sociale temps plein - points: - 0: 0% - 1.1: 0% - 1.4: 3.1% + multiplication: + assiette: assiette des cotisations + taux: + taux progressif: + assiette: assiette des cotisations + multiplicateur: plafond sécurité sociale temps plein + tranches: + - plafond: 110% + taux: 0% + - plafond: 140% + taux: 3.1% dirigeant . indépendant . cotisations et contributions . exonérations: période: flexible @@ -1001,8 +1007,8 @@ dirigeant . indépendant . cotisations et contributions . exonérations . ZFU: multiplication: assiette: cotisations . maladie # TODO : ceci n'est pas bon (le plafond est sur le revenu exonéré, et est proratisé en début / fin d'éxo) - plafond: 3042 heures/an * SMIC horaire taux: taux + plafond: 3042 heures/an * SMIC horaire dirigeant . indépendant . cotisations et contributions . exonérations . âge: question: Bénéficiez-vous du dispositif d'exonération "âge" @@ -1024,30 +1030,41 @@ dirigeant . indépendant . cotisations et contributions . exonérations . invali dirigeant . indépendant . cotisations et contributions . exonérations . ZFU . taux: titre: taux exonération ZFU formule: - barème continu: + taux progressif: assiette: établissement . ZFU . durée d'implantation en fin d'année [an] retourne seulement le taux: oui variations: - si: entreprise . effectif < 5 alors: - points: - 0: 100% - 5: 100% - 6: 60% - 10: 60% - 11: 40% - 12: 40% - 13: 20% - 14: 20% - 15: 0% + tranches: + - plafond: 5 ans + taux: 100% + - plafond: 6 ans + taux: 60% + - plafond: 10 ans + taux: 60% + - plafond: 11 ans + taux: 40% + - plafond: 12 ans + taux: 40% + - plafond: 13 ans + taux: 20% + - plafond: 14 ans + taux: 20% + - plafond: 15 ans + taux: 0% - sinon: - points: - 0: 100% - 5: 100% - 6: 60% - 7: 40% - 8: 20% - 9: 0% + tranches: + - plafond: 5 ans + taux: 100% + - plafond: 6 ans + taux: 60% + - plafond: 7 ans + taux: 40% + - plafond: 8 ans + taux: 20% + - plafond: 9 ans + taux: 0% dirigeant . indépendant . cotisations et contributions . cotisations . maladie domiciliation fiscale étranger: applicable si: situation personnelle . domiciliation fiscale à l'étranger diff --git a/publicode/rules/entreprise-établissement.yaml b/publicode/rules/entreprise-établissement.yaml index d454cff62..7335748df 100644 --- a/publicode/rules/entreprise-établissement.yaml +++ b/publicode/rules/entreprise-établissement.yaml @@ -83,15 +83,13 @@ entreprise . impôt sur les sociétés: unité: €/an formule: barème: - assiette: bénéfice [€/an] + assiette: bénéfice tranches: - - en-dessous de: 38120 - taux: 15% - - de: 38120 - à: 500000 - taux: 28% - - au-dessus de: 500000 - taux: 33.3% + - taux: 15% + plafond: 38120 €/an + - taux: 28% + plafond: 500000 €/an + - taux: 33.3% références: fiche service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23575 @@ -279,8 +277,8 @@ entreprise . taxe sur les salaires: assiette: barème franchise: 1200 €/an décote: - plafond: 2040 €/an taux: 75% + plafond: 2040 €/an abattement: abattement associations entreprise . catégorie d'activité: diff --git a/publicode/rules/impôt.yaml b/publicode/rules/impôt.yaml index b3ca2d194..991c2226a 100644 --- a/publicode/rules/impôt.yaml +++ b/publicode/rules/impôt.yaml @@ -128,21 +128,17 @@ impôt . impôt sur le revenu: unité par défaut: €/an formule: barème: - assiette: revenu abattu [€/an] + assiette: revenu abattu tranches: - - en-dessous de: 10064 - taux: 0% - - de: 10064 - à: 25659 - taux: 11% - - de: 25659 - à: 73369 - taux: 30% - - de: 73369 - à: 157806 - taux: 41% - - au-dessus de: 157806 - taux: 45% + - taux: 0% + plafond: 10064 €/an + - taux: 11% + plafond: 25659 €/an + - taux: 30% + plafond: 73369 €/an + - taux: 41% + plafond: 157806 €/an + - taux: 45% exemples: - nom: Haut salaire de ~ 10 000€ mensuels situation: @@ -158,8 +154,8 @@ impôt . impôt sur le revenu à payer: allègement: assiette: impôt sur le revenu décote: - plafond: 1717 €/an taux: 45.25% + plafond: 1717 €/an exemples: - nom: Salaire d'un cadre situation: @@ -185,15 +181,13 @@ impôt . CEHR: note: Attention, ce barème concerne les foyers célibataires. Pour les couples, le barème est adapté pour ne pas leur appliquer la même imposition alors qu'ils sont individuellement deux fois moins riches. formule: barème: - assiette: revenu fiscal de référence [€/an] + assiette: revenu fiscal de référence tranches: - - en-dessous de: 250000 - taux: 0% - - de: 250000 - à: 500000 - taux: 3% - - au-dessus de: 500000 - taux: 4% + - taux: 0% + plafond: 250000 €/an + - taux: 3% + plafond: 500000 €/an + - taux: 4% références: contribution exceptionnelle sur les hauts revenus: https://www.service-public.fr/particuliers/vosdroits/F31130 Article 223 sexies du Code général des impôts: https://www.legifrance.gouv.fr/affichCode.do?idSectionTA=LEGISCTA000025049019&cidTexte=LEGITEXT000006069577 @@ -202,174 +196,94 @@ impôt . CEHR: impôt . taux neutre d'impôt sur le revenu . barème Guadeloupe Réunion Martinique: icônes: 🇬🇵🇷🇪 🇲🇶 formule: - barème linéaire: - assiette: revenu imposable [€/mois] - retourne seulement le taux: oui + grille: + assiette: revenu imposable tranches: - - de: 0 - à: 1625 - taux: 0% - - - de: 1626 - à: 1723 - taux: 0.5% - - - de: 1724 - à: 1899 - taux: 1.3% - - - de: 1900 - à: 2074 - taux: 2.1% - - - de: 2075 - à: 2291 - taux: 2.9% - - - de: 2292 - à: 2416 - taux: 3.5% - - - de: 2417 - à: 2499 - taux: 4.1% - - - de: 2500 - à: 2749 - taux: 5.3% - - - de: 2750 - à: 3399 - taux: 7.5% - - - de: 3400 - à: 4349 - taux: 9.9% - - - de: 4350 - à: 4941 - taux: 11.9% - - - de: 4942 - à: 5724 - taux: 13.8% - - - de: 5725 - à: 6857 - taux: 15.8% - - - de: 6858 - à: 7624 - taux: 17.9% - - - de: 7625 - à: 8666 - taux: 20% - - - de: 8667 - à: 11916 - taux: 24% - - - de: 11917 - à: 15832 - taux: 28% - - - de: 15833 - à: 24166 - taux: 33% - - - de: 24167 - à: 52824 - taux: 38% - - - au-dessus de: 52825 - taux: 43% + - montant: 0% + plafond: 1626 €/mois + - montant: 0.5% + plafond: 1724 €/mois + - montant: 1.3% + plafond: 1900 €/mois + - montant: 2.1% + plafond: 2075 €/mois + - montant: 2.9% + plafond: 2292 €/mois + - montant: 3.5% + plafond: 2417 €/mois + - montant: 4.1% + plafond: 2500 €/mois + - montant: 5.3% + plafond: 2750 €/mois + - montant: 7.5% + plafond: 3400 €/mois + - montant: 9.9% + plafond: 4350 €/mois + - montant: 11.9% + plafond: 4942 €/mois + - montant: 13.8% + plafond: 5725 €/mois + - montant: 15.8% + plafond: 6858 €/mois + - montant: 17.9% + plafond: 7625 €/mois + - montant: 20% + plafond: 8667 €/mois + - montant: 24% + plafond: 11917 €/mois + - montant: 28% + plafond: 15833 €/mois + - montant: 33% + plafond: 24167 €/mois + - montant: 38% + plafond: 52825 €/mois + - montant: 43% impôt . taux neutre d'impôt sur le revenu . barème Guyane Mayotte: icônes: 🇬🇾 🇾🇹 formule: - barème linéaire: - assiette: revenu imposable [€/mois] - retourne seulement le taux: oui + grille: + assiette: revenu imposable tranches: - - de: 0 - à: 1740 - taux: 0% - - - de: 1741 - à: 1882 - taux: 0.5% - - - de: 1883 - à: 2099 - taux: 1.3% - - - de: 2100 - à: 2366 - taux: 2.1% - - - de: 2367 - à: 2457 - taux: 2.9% - - - de: 2458 - à: 2541 - taux: 3.5% - - - de: 2542 - à: 2624 - taux: 4.1% - - - de: 2625 - à: 2916 - taux: 5.3% - - - de: 2917 - à: 4024 - taux: 7.5% - - - de: 4025 - à: 5207 - taux: 9.9% - - - de: 5208 - à: 5874 - taux: 11.9% - - - de: 5875 - à: 6816 - taux: 13.8% - - - de: 6817 - à: 7499 - taux: 15.8% - - - de: 7500 - à: 8307 - taux: 17.9% - - - de: 8308 - à: 9641 - taux: 20% - - - de: 9642 - à: 12970 - taux: 24% - - - de: 12971 - à: 16499 - taux: 28% - - - de: 16500 - à: 26442 - taux: 33% - - - de: 26443 - à: 55814 - taux: 38% - - - au-dessus de: 55815 - taux: 43% + - montant: 0% + plafond: 1740 €/mois + - montant: 0.5% + plafond: 1883 €/mois + - montant: 1.3% + plafond: 2100 €/mois + - montant: 2.1% + plafond: 2367 €/mois + - montant: 2.9% + plafond: 2458 €/mois + - montant: 3.5% + plafond: 2542 €/mois + - montant: 4.1% + plafond: 2625 €/mois + - montant: 5.3% + plafond: 2917 €/mois + - montant: 7.5% + plafond: 4025 €/mois + - montant: 9.9% + plafond: 5208 €/mois + - montant: 11.9% + plafond: 5875 €/mois + - montant: 13.8% + plafond: 6817 €/mois + - montant: 15.8% + plafond: 7500 €/mois + - montant: 17.9% + plafond: 8308 €/mois + - montant: 20% + plafond: 9642 €/mois + - montant: 24% + plafond: 12971 €/mois + - montant: 28% + plafond: 16500 €/mois + - montant: 33% + plafond: 26443 €/mois + - montant: 38% + plafond: 55815 €/mois + - montant: 43% impôt . taux neutre d'impôt sur le revenu: description: > @@ -386,89 +300,48 @@ impôt . taux neutre d'impôt sur le revenu: - établissement . localisation . département = 'Mayotte' alors: barème Guyane Mayotte - sinon: - barème linéaire: - assiette: revenu imposable [€/mois] - retourne seulement le taux: oui + grille: + assiette: revenu imposable tranches: - - de: 0 - à: 1417 - taux: 0% - - - de: 1418 - à: 1471 - taux: 0.5% - - - de: 1472 - à: 1566 - taux: 1.3% - - - de: 1567 - à: 1672 - taux: 2.1% - - - de: 1673 - à: 1786 - taux: 2.9% - - - de: 1787 - à: 1882 - taux: 3.5% - - - de: 1883 - à: 2007 - taux: 4.1% - - - de: 2008 - à: 2375 - taux: 5.3% - - - de: 2376 - à: 2719 - taux: 7.5% - - - de: 2720 - à: 3097 - taux: 9.9% - - - de: 3098 - à: 3486 - taux: 11.9% - - - de: 3487 - à: 4068 - taux: 13.8% - - - de: 4069 - à: 4877 - taux: 15.8% - - - de: 4878 - à: 6103 - taux: 17.9% - - - de: 6104 - à: 7624 - taux: 20% - - - de: 7625 - à: 10582 - taux: 24% - - - de: 10583 - à: 14332 - taux: 28% - - - de: 14333 - à: 22499 - taux: 33% - - - de: 22500 - à: 48195 - taux: 38% - - - au-dessus de: 48196 - taux: 43% - + - montant: 0% + plafond: 1418 €/mois + - montant: 0.5% + plafond: 1472 €/mois + - montant: 1.3% + plafond: 1567 €/mois + - montant: 2.1% + plafond: 1673 €/mois + - montant: 2.9% + plafond: 1787 €/mois + - montant: 3.5% + plafond: 1883 €/mois + - montant: 4.1% + plafond: 2008 €/mois + - montant: 5.3% + plafond: 2376 €/mois + - montant: 7.5% + plafond: 2720 €/mois + - montant: 9.9% + plafond: 3098 €/mois + - montant: 11.9% + plafond: 3487 €/mois + - montant: 13.8% + plafond: 4069 €/mois + - montant: 15.8% + plafond: 4878 €/mois + - montant: 17.9% + plafond: 6104 €/mois + - montant: 20% + plafond: 7625 €/mois + - montant: 24% + plafond: 10583 €/mois + - montant: 28% + plafond: 14333 €/mois + - montant: 33% + plafond: 22500 €/mois + - montant: 38% + plafond: 48196 €/mois + - montant: 43% références: Explication de l'impôt neutre: https://www.economie.gouv.fr/prelevement-a-la-source/taux-prelevement#taux-non-personnalise BOFIP: http://bofip.impots.gouv.fr/bofip/11255-PGP.html diff --git a/publicode/rules/protection-sociale.yaml b/publicode/rules/protection-sociale.yaml index bb157e371..efc94459f 100644 --- a/publicode/rules/protection-sociale.yaml +++ b/publicode/rules/protection-sociale.yaml @@ -49,8 +49,8 @@ protection sociale . retraite . base: titre: pension de retraite de base formule: multiplication: - plafond: plafond sécurité sociale temps plein taux: taux de la pension + plafond: plafond sécurité sociale temps plein assiette: revenu moyen note: Les impatriés bénéficient d'une exonération de cotisation vieillesse. En contrepartie, ils n'acquièrent aucun droit pendant la durée d'exonération. références: @@ -98,24 +98,20 @@ protection sociale . retraite . trimestres validés par an . trimestres indépen protection sociale . retraite . trimestres validés par an . barème trimestres générique: unité: trimestres validés/an formule: - barème linéaire: + grille: unité: trimestres validés/an - assiette: revenu moyen [€/an] + assiette: revenu moyen multiplicateur: SMIC horaire tranches: - - en-dessous de: 150 - montant: 0 - - de: 150 - à: 300 - montant: 1 - - de: 300 - à: 450 - montant: 2 - - de: 450 - à: 600 - montant: 3 - - au-dessus de: 600 - montant: 4 + - montant: 0 + plafond: 150 heures/an + - montant: 1 + plafond: 300 heures/an + - montant: 2 + plafond: 450 heures/an + - montant: 3 + plafond: 600 heures/an + - montant: 4 références: cnav.fr: https://www.legislation.cnav.fr/Pages/bareme.aspx?Nom=salaire_validant_un_trimestre_montant_bar @@ -127,63 +123,51 @@ protection sociale . retraite . trimestres validés par an . trimestres auto-ent variations: - si: entreprise . catégorie d'activité = 'libérale' alors: - barème linéaire: + grille: unité: trimestres validés/an - assiette: entreprise . chiffre d'affaires [€/an] + assiette: entreprise . chiffre d'affaires tranches: - - en-dessous de: 2880 - montant: 0 - - de: 2880 - à: 5062 - montant: 1 - - de: 5062 - à: 7266 - montant: 2 - - de: 7266 - à: 9675 - montant: 3 - - au-dessus de: 9675 - montant: 4 + - montant: 0 + plafond: 2880 €/an + - montant: 1 + plafond: 5062 €/an + - montant: 2 + plafond: 7266 €/an + - montant: 3 + plafond: 9675 €/an + - montant: 4 - si: une de ces conditions: - entreprise . catégorie d'activité . service ou vente = 'vente' - entreprise . catégorie d'activité . restauration ou hébergement alors: - barème linéaire: + grille: unité: trimestres validés/an - assiette: entreprise . chiffre d'affaires [€/an] + assiette: entreprise . chiffre d'affaires tranches: - - en-dessous de: 4137 - montant: 0 - - de: 4137 - à: 7286 - montant: 1 - - de: 7286 - à: 10426 - montant: 2 - - de: 10426 - à: 20740 - montant: 3 - - au-dessus de: 20740 - montant: 4 + - montant: 0 + plafond: 4137 €/an + - montant: 1 + plafond: 7286 €/an + - montant: 2 + plafond: 10426 €/an + - montant: 3 + plafond: 20740 €/an + - montant: 4 - sinon: - barème linéaire: + grille: unité: trimestres validés/an - assiette: entreprise . chiffre d'affaires [€/an] + assiette: entreprise . chiffre d'affaires tranches: - - en-dessous de: 2412 - montant: 0 - - de: 2412 - à: 4239 - montant: 1 - - de: 4239 - à: 6071 - montant: 2 - - de: 6071 - à: 12030 - montant: 3 - - au-dessus de: 12030 - montant: 4 + - montant: 0 + plafond: 2412 €/an + - montant: 1 + plafond: 4239 €/an + - montant: 2 + plafond: 6071 €/an + - montant: 3 + plafond: 12030 €/an + - montant: 4 références: service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23369 @@ -334,8 +318,8 @@ protection sociale . santé . indemnités journalières . salarié: formule: multiplication: assiette: revenu moyen [€/jour] - plafond: 1.8 * contrat salarié . SMIC temps plein [€/jour] taux: 50% + plafond: 1.8 * contrat salarié . SMIC temps plein [€/jour] reférences: service-public.fr: https://www.service-public.fr/particuliers/vosdroits/F3053 diff --git a/publicode/rules/salarié.yaml b/publicode/rules/salarié.yaml index c31e7745f..09b2f869e 100644 --- a/publicode/rules/salarié.yaml +++ b/publicode/rules/salarié.yaml @@ -255,38 +255,46 @@ contrat salarié . CDD . CPF: valeur attendue: null contrat salarié . CDD . compensation pour congés non pris: + titre: indemnité de congés payés indemnité: destinataire: salarié dû par: employeur - description: | - Le salarié en CDD bénéficie des mêmes droits à congés payés que le salarié en CDI. Il acquiert et prend ses congés payés dans les mêmes conditions. + description: >- + Le salarié en CDD bénéficie des mêmes droits à congés payés que le salarié + en CDI. Il acquiert et prend ses congés payés dans les mêmes conditions. + + Il est cependant courant que le salarié ne puisse pas prendre tous ses + congés avant le terme de son contrat, il bénéficie alors d'une indemnité + compensatrice de congés payés versée par l'employeur. + + Il existe deux méthodes pour calculer l'indemnité de congés non pris. + + ### Méthode "du dixième" + + Ce mode de calcul sera le plus souvent favorable au salarié lorsque celui-ci + a accompli des heures supplémentaires. Une indemnité égale au dixième de la + rémunération brute totale perçue par le salarié au cours de la période de + référence. + + ### Méthode "maintien du salaire" + + Cette méthode sera le plus souvent favorable au salarié lorsque celui-ci a + bénéficié d’une augmentation de salaire. + + Pour effectuer le calcul, l'employeur peut tenir compte soit : + - de l'horaire réel du mois, + - du nombre moyen de jours ouvrables (ou ouvrés), + - du nombre réel de jours ouvrables (ou ouvrés). - Il est cependant courant que le salarié ne puisse pas prendre tous ses congés avant le terme de son contrat, il bénéficie alors d'une indemnité compensatrice de congés payés versée par l'employeur. unité: €/mois - - non applicable si: - une de ces conditions: - - événement . poursuite du CDD en CDI - # TODO Y a-t-il d'autres conditions ? Sinon supprimer la liste + non applicable si: événement . poursuite du CDD en CDI formule: le maximum de: - - description: Méthode "du dixième" - note: | - Ce mode de calcul sera le plus souvent favorable au salarié lorsque celui-ci a accompli des heures supplémentaires. - > Une indemnité égale au dixième de la rémunération brute totale perçue par le salarié au cours de la période de référence. - multiplication: + - multiplication: assiette: assiette mensuelle taux: 10% facteur: proportion congés non pris - - description: Méthode "maintien du salaire" - note: | - Cette méthode sera le plus souvent favorable au salarié lorsque celui-ci a bénéficié d’une augmentation de salaire. - > Pour effectuer le calcul, l'employeur peut tenir compte soit : - - de l'horaire réel du mois, - - du nombre moyen de jours ouvrables (ou ouvrés), - - du nombre réel de jours ouvrables (ou ouvrés). - référence: https://www.service-public.fr/particuliers/vosdroits/F33359 - multiplication: + - multiplication: assiette: salaire journalier facteur: congés non pris / durée contrat exemples: @@ -316,13 +324,14 @@ contrat salarié . CDD . compensation pour congés non pris: note: | L'indemnité est versée à la fin du contrat, sauf si le CDD se poursuit par un CDI. À noter, la loi El Khomri modifie l'article L3141-12: - - avant : Les congés peuvent être pris dès l'ouverture des droits [...] - - maintenant : Les congés peuvent être pris dès l’embauche [...] + - avant : Les congés peuvent être pris dès l'ouverture des droits + - maintenant : Les congés peuvent être pris dès l’embauche références: Fiche service-public.gouv.fr: https://www.service-public.fr/particuliers/vosdroits/F2931 Code du travail - Article L3141-24: https://www.legifrance.gouv.fr/affichCodeArticle.do?cidTexte=LEGITEXT000006072050&idArticle=LEGIARTI000006902661&dateTexte=&categorieLien=cid Congés payés et contrat CDD: https://www.easycdd.com/LEGISLATION-CDD/L-embauche-le-suivi-du-contrat-CDD-les-incidents-frequents/Conges-payes-et-contrat-CDD assiette de l'indemnité, circulaire DRT 18 du 30 octobre 1990: http://conseillerdusalarie.free.fr/Docs/TextesFrance/19901030Circulaire_DRT_90_18_du_30_octobre_1990_CDD_Travail_temporaire.htm + Méthode du maintien de salaire: https://www.service-public.fr/particuliers/vosdroits/F33359 contrat salarié . CDD . compensation pour congés non pris . proportion congés non pris: unité: '%' @@ -1449,14 +1458,14 @@ contrat salarié . réduction ACRE: contrat salarié . réduction ACRE . taux: titre: taux ACRE formule: - barème continu: + taux progressif: assiette: cotisations . assiette multiplicateur: plafond sécurité sociale temps plein - points: - 0: 100% - 0.75: 100% - 1: 0% - retourne seulement le taux: oui + tranches: + - plafond: 75% + taux: 100% + - plafond: 100% + taux: 0% contrat salarié . cotisations . salariales . réduction heures supplémentaires: cotisation: @@ -1639,12 +1648,11 @@ contrat salarié . temps de travail . heures supplémentaires . majoration: formule: barème: assiette: heures supplémentaires - multiplicateur: 1 heure/semaine * période . semaines par mois + multiplicateur: période . semaines par mois tranches: - - en-dessous de: 8 - taux: 25% - - au-dessus de: 8 - taux: 50% + - taux: 25% + plafond: 8 heures/semaine + - taux: 50% contrat salarié . temps de travail . heures complémentaires: description: > @@ -1848,21 +1856,19 @@ contrat salarié . contribution d'équilibre général: - attributs: dû par: employeur tranches: - - en-dessous de: 1 - taux: 1.29% - - de: 1 - à: 8 - taux: 1.62% + - taux: 1.29% + plafond: 1 + - taux: 1.62% + plafond: 8 - attributs: dû par: salarié assiette: cotisations . assiette . salariale tranches: - - en-dessous de: 1 - taux: 0.86% - - de: 1 - à: 8 - taux: 1.08% + - taux: 0.86% + plafond: 1 + - taux: 1.08% + plafond: 8 références: calcul des cotisations: https://www.agirc-arrco.fr/ce-qui-change-au-1er-janvier-2019/vous-etes-une-entreprise-tiers-declarant/ @@ -1902,20 +1908,18 @@ contrat salarié . retraite complémentaire: - attributs: dû par: employeur tranches: - - en-dessous de: 1 - taux: taux employeur tranche 1 - - de: 1 - à: 8 - taux: taux employeur tranche 2 + - taux: taux employeur tranche 1 + plafond: 1 + - taux: taux employeur tranche 2 + plafond: 8 - attributs: dû par: salarié assiette: cotisations . assiette . salariale tranches: - - en-dessous de: 1 - taux: taux salarié tranche 1 - - de: 1 - à: 8 - taux: taux salarié tranche 2 + - taux: taux salarié tranche 1 + plafond: 1 + - taux: taux salarié tranche 2 + plafond: 8 références: calcul des cotisations: https://www.agirc-arrco.fr/ce-qui-change-au-1er-janvier-2019/vous-etes-une-entreprise-tiers-declarant/ @@ -1941,8 +1945,8 @@ contrat salarié . AGS: formule: multiplication: assiette: cotisations . assiette - plafond: 4 * plafond sécurité sociale taux: 0.15% + plafond: 4 * plafond sécurité sociale contrat salarié . allocations familiales: cotisation: @@ -2170,14 +2174,13 @@ contrat salarié . assiette CSG et CRDS: contrat salarié . assiette CSG et CRDS . assiette abattue: formule: barème: - assiette: cotisations . assiette [€/mois] + assiette: cotisations . assiette multiplicateur: plafond sécurité sociale # c'est en fait un abattement de 1,75% sur la partie en-dessous de 4 fois le plafond tranches: - - en-dessous de: 4 - taux: 98.25% - - au-dessus de: 4 - taux: 100% + - taux: 98.25% + plafond: 4 + - taux: 100% contrat salarié . CSG . assiette heures supplémentaires et complémentaires défiscalisées: formule: @@ -2410,8 +2413,8 @@ contrat salarié . prévoyance obligatoire cadre: formule: multiplication: assiette: cotisations . assiette - plafond: plafond sécurité sociale taux: 1.5% + plafond: plafond sécurité sociale # TODO attention : il semblerait que ce 1.5% englobe aussi la complémentaire santé ! La confusion serait entretenue par les organismes de prévoyance... contrat salarié . taxe d'apprentissage: @@ -2528,15 +2531,13 @@ contrat salarié . taxe sur les salaires . barème: formule: barème: - assiette: assiette [€/an] + assiette: assiette tranches: - - en-dessous de: 7799 - taux: 4.25% - - de: 7799 - à: 15572 - taux: 8.5% - - au-dessus de: 15572 - taux: 13.6% + - taux: 4.25% + plafond: 7799 €/an + - taux: 8.5% + plafond: 15572 €/an + - taux: 13.6% exemples: - nom: salaire médian situation: @@ -2641,16 +2642,16 @@ contrat salarié . vieillesse: taux: taux salarié non plafonné - nom: plafonnée - plafond: plafond sécurité sociale taux: taux salarié plafonné + plafond: plafond sécurité sociale - attributs: dû par: employeur composantes: - nom: non plafonnée taux: taux employeur non plafonné - nom: plafonnée - plafond: plafond sécurité sociale taux: taux employeur plafonné + plafond: plafond sécurité sociale exemples: - nom: SMIC diff --git a/source/components/CurrencyInput/CurrencyInput.tsx b/source/components/CurrencyInput/CurrencyInput.tsx index 2cd028007..49306fe65 100644 --- a/source/components/CurrencyInput/CurrencyInput.tsx +++ b/source/components/CurrencyInput/CurrencyInput.tsx @@ -89,7 +89,7 @@ export default function CurrencyInput({ } onValueChange={({ value }) => { setCurrentValue(value) - nextValue.current = value.toString().replace(/^-/, '') + nextValue.current = value.toString().replace('-', '') }} onChange={handleChange} value={currentValue.toString().replace('.', decimalSeparator)} diff --git a/source/components/StackedBarChart.tsx b/source/components/StackedBarChart.tsx index bdb4ba000..9c8f72df8 100644 --- a/source/components/StackedBarChart.tsx +++ b/source/components/StackedBarChart.tsx @@ -62,11 +62,11 @@ function integerAndDecimalParts(value: number) { // returned values is always 100. For instance: [60, 30, 10]. export function roundedPercentages(values: Array) { const sum = (a: number = 0, b: number) => a + b - const total = values.reduce(sum) + const total = values.reduce(sum, 0) const percentages = values.map(value => integerAndDecimalParts((value / total) * 100) ) - const totalRoundedPercentage = percentages.map(v => v.integer).reduce(sum) + const totalRoundedPercentage = percentages.map(v => v.integer).reduce(sum, 0) const indexesToIncrement = percentages .map((percentage, index) => ({ ...percentage, index })) .sort((a, b) => b.decimal - a.decimal) diff --git a/source/components/rule/Algorithm.tsx b/source/components/rule/Algorithm.tsx index ca0b279f0..8f5601552 100644 --- a/source/components/rule/Algorithm.tsx +++ b/source/components/rule/Algorithm.tsx @@ -19,7 +19,7 @@ let Conditions = ({ parentDependency.nodeValue === false && ( ) ), @@ -63,7 +63,6 @@ export default function Algorithm({ rule, showValues }) { !!Object.keys(formula).length && !path(['formule', 'explanation', 'une possibilité'], rule) && formula.explanation?.category !== 'number' - return (
diff --git a/source/engine/error.ts b/source/engine/error.ts index b0ea437ba..14f7327a6 100644 --- a/source/engine/error.ts +++ b/source/engine/error.ts @@ -1,12 +1,26 @@ import { coerceArray } from '../utils' export function syntaxError( - dottedName: string, + rules: string[] | string, message: string, originalError: Error ) { throw new Error( - `[ Erreur syntaxique ] -➡️ Dans la règle \`${dottedName}\`, + `\n[ Erreur syntaxique ] +➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` +✖️ ${message} + ${originalError && originalError.message} +` + ) +} + +export function evaluationError( + rules: string[] | string, + message: string, + originalError?: Error +) { + throw new Error( + `\n[ Erreur d'évaluation ] +➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` ✖️ ${message} ${originalError && originalError.message} ` @@ -19,18 +33,22 @@ export function typeWarning( originalError?: Error ) { console.warn( - `[ Erreur de type ] -➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`, + `\n[ Erreur de type ] +➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` ✖️ ${message} ${originalError && originalError.message} ` ) } -export function warning(dottedName: string, message: string, solution: string) { +export function warning( + rules: string[] | string, + message: string, + solution: string +) { console.warn( - `[ Avertissement ] -➡️ Dans la règle \`${dottedName}\`, + `\n[ Avertissement ] +➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\` ⚠️ ${message} 💡${solution} ` diff --git a/source/engine/evaluation.js b/source/engine/evaluation.js index cd6abf201..df457a620 100644 --- a/source/engine/evaluation.js +++ b/source/engine/evaluation.js @@ -43,20 +43,25 @@ export let evaluateNode = (cache, situationGate, parsedRules, node) => { return evaluatedNode } const sameUnitValues = (explanation, contextRule, mecanismName) => { - const unit = explanation.map(n => n.unit).find(Boolean) + const firstNodeWithUnit = explanation.find(node => !!node.unit) + if (!firstNodeWithUnit) { + return [undefined, explanation.map(({ nodeValue }) => nodeValue)] + } const values = explanation.map(node => { try { - return convertNodeToUnit(unit, node).nodeValue + return convertNodeToUnit(firstNodeWithUnit?.unit, node).nodeValue } catch (e) { typeWarning( contextRule, - `'${node.name}' a une unité incompatible avec celle du mécanisme ${mecanismName}`, + `Dans le mécanisme ${mecanismName}, les unités des éléments suivants sont incompatibles entre elles : \n\t\t${node?.name || + node?.rawNode}\n\t\t${firstNodeWithUnit?.name || + firstNodeWithUnit?.rawNode}'`, e ) return node.nodeValue } }) - return [unit, values] + return [firstNodeWithUnit.unit, values] } export let evaluateArray = (reducer, start) => ( @@ -144,7 +149,12 @@ export let evaluateObject = (objectShape, effect) => ( let transforms = map(k => [k, evaluateOne], keys(objectShape)), automaticExplanation = evolve(fromPairs(transforms))(node.explanation) // the result of effect can either be just a nodeValue, or an object {additionalExplanation, nodeValue}. The latter is useful for a richer JSX visualisation of the mecanism : the view should not duplicate code to recompute intermediate values (e.g. for a marginal 'barème', the marginal 'tranche') - let evaluated = effect(automaticExplanation, cache, situationGate, parsedRules), + let evaluated = effect( + automaticExplanation, + cache, + situationGate, + parsedRules + ), explanation = is(Object, evaluated) ? { ...automaticExplanation, ...evaluated.additionalExplanation } : automaticExplanation, diff --git a/source/engine/grammar.ne b/source/engine/grammar.ne index 86cf964d1..9cc736d1b 100644 --- a/source/engine/grammar.ne +++ b/source/engine/grammar.ne @@ -24,6 +24,7 @@ const lexer = moo.compile({ '[': '[', ']': ']', comparison: ['>','<','>=','<=','=','!='], + infinity: 'Infinity', words: new RegExp(words), number: new RegExp(numberRegExp), string: /'[ \t\.'a-zA-Z\-\u00C0-\u017F0-9 ]+'/, @@ -106,6 +107,7 @@ boolean -> number -> %number {% number %} + | %infinity {% number %} | %number (%space):? Unit {% numberWithUnit %} string -> %string {% string %} \ No newline at end of file diff --git a/source/engine/known-mecanisms.yaml b/source/engine/known-mecanisms.yaml index cc2370e9d..2564d1a6f 100644 --- a/source/engine/known-mecanisms.yaml +++ b/source/engine/known-mecanisms.yaml @@ -134,37 +134,41 @@ rend non applicable: barème: type: numeric - description: | - C'est un barème en taux marginaux, mécanisme de calcul connu son utilisation dans le calcul de l'impôt sur le revenu. + description: >- + C'est un barème en taux marginaux, mécanisme de calcul connu son utilisation + dans le calcul de l'impôt sur le revenu. - L'assiette est décomposée en plusieurs tranches, qui sont multipliées par un taux spécifique. + L'assiette est décomposée en plusieurs tranches, qui sont multipliées par un + taux spécifique. - Les tranches sont souvent exprimées sous forme de facteurs d'une variable que l'on appelle `multiplicateur`, par exemple `1 x le plafond de la sécurité sociale`. + Les tranches sont souvent exprimées sous forme de facteurs d'une variable + que l'on appelle `multiplicateur`, par exemple `1 x le plafond de la + sécurité sociale`. -barème linéaire: +grille: type: numeric - description: | - C'est un barème en taux non marginaux, très simple. C'est le mécanisme de calcul de l'impôt neutre, aussi appelé impôt non personnalisé. - Il est composé de tranches qui se suivent. Il suffit de trouver l'assiette qui correspond à la tranche, et de multiplier le taux associé avec l'assiette. - Un montant fixe pour chaque tranche peut aussi remplacer le taux, rendant le barème encore plus simple, mais moins "juste", car moins continu. + description: >- + C'est un barème sous la forme d'une grille de correspondance. C'est le + mécanisme de calcul de l'impôt neutre, aussi appelé impôt non personnalisé. -barème continu: + Il est composé de tranches qui se suivent. Il suffit de trouver l'assiette + qui correspond à la tranche, et de selectionner le montant associé à + l'assiette. + +taux progressif: type: numeric - description: | - Désolé, ce mécanisme est quelque peu compliqué, accrochez-vous ! + description: >- + Ce mécanisme permet de calculer un taux progressif. On spécifie pour chaque + tranche le plafond et le taux associé. Le taux effectif renvoyé est calculé + en lissant la différence de taux entre la borne inférieure et supérieure de + l'assiette + + > Par exemple, si nous nous avons les tranches suivantes : + - taux: 50% / plafond: 0 + - taux: 100% / plafond: 1000 - Le barème définit des points, constitués d'un seuil et d'un taux correspondant. Par exemple `1 : 6%` et `2 : 8%`. L'assiette du barème est placée au bon endroit entre ces seuils, et on en déduit le taux qui lui correspond. - - Dans notre exemple, si l'assiette vaut 1, le taux est 6%. Si elle vaut 2, le taux est 8%. Et si elle est entre les deux, par exemple 1.5, alors le taux est 7%. Et ainsi de suite. - - Dans ce type de barème, il y a souvent un multiplicateur de l'assiette, par exemple le plafond de la sécurité sociale : les seuils sont des petits nombres comme 1 ou 1.5, mais ils correspondent en réalité à `1 * 3311€` et `1.5 * 3311€`. - - L'option `retourne seulement le taux: oui` permet d'obtenir un taux pour une assiette donnée, pour l'appliquer ensuite à une autre assiette. - -complément: - type: numeric - description: | - Complète une base pour atteindre un seuil minimal + > Pour une assiette de 500, le taux retourné sera 75%, car il correspond au + taux situé à la moitié de la tranche correspondante. composantes: type: numeric @@ -196,5 +200,12 @@ durée: synchronisation: type: object description: | - Pour éviter trop de saisies à l'utilisateur, certaines informations sont récupérées à partir de ce que l'on appelle des API. Ce sont des services auxquels ont fait appel pour obtenir des informations sur un sujet précis. Par exemple, l'État français fournit gratuitement l'API géo, qui permet à partir du nom d'une ville, d'obtenir son code postal, son département, la population etc. - Ce mécanismes `synchronisation` permet de faire le lien entre les règles de notre système et les réponses de ces API. + Pour éviter trop de saisies à l'utilisateur, certaines informations sont + récupérées à partir de ce que l'on appelle des API. Ce sont des services + auxquels ont fait appel pour obtenir des informations sur un sujet précis. + Par exemple, l'État français fournit gratuitement l'API géo, qui permet à + partir du nom d'une ville, d'obtenir son code postal, son département, la + population etc. + + Ce mécanismes `synchronisation` permet de faire le lien entre les règles de + notre système et les réponses de ces API. diff --git a/source/engine/mecanismViews/Barème.js b/source/engine/mecanismViews/Barème.js deleted file mode 100644 index 8f52724d0..000000000 --- a/source/engine/mecanismViews/Barème.js +++ /dev/null @@ -1,238 +0,0 @@ -import classNames from 'classnames' -import { ShowValuesContext } from 'Components/rule/ShowValuesContext' -import RuleLink from 'Components/RuleLink' -import { numberFormatter } from 'Engine/format' -import { trancheValue } from 'Engine/mecanisms/barème' -import { inferUnit, serializeUnit } from 'Engine/units' -import { identity } from 'ramda' -import React, { useContext } from 'react' -import { Trans } from 'react-i18next' -import { makeJsx } from '../evaluation' -import './Barème.css' -import { Node, NodeValuePointer } from './common' - -export let BarèmeAttributes = ({ explanation, lazyEval = identity }) => { - const multiplicateur = lazyEval(explanation['multiplicateur']) - const multiplicateurAcronym = multiplicateur?.explanation?.acronyme - - return ( - <> -
  • - - assiette:{' '} - - {makeJsx(lazyEval(explanation.assiette))} -
  • - {explanation['multiplicateur'] && - explanation['multiplicateur'].nodeValue !== 1 && - !multiplicateurAcronym && ( -
  • - - multiplicateur:{' '} - - {makeJsx(multiplicateur)} -
  • - )} - - ) -} - -let Component = function Barème({ - language, - nodeValue, - explanation, - barèmeType, - lazyEval, - unit -}) { - const showValues = useContext(ShowValuesContext) - const multiplicateur = lazyEval(explanation?.multiplicateur) - return ( - - - - - - - - {showValues && - !explanation.returnRate && - explanation.tranches[0].taux != null && ( - - )} - - - - {explanation.tranches.map(tranche => ( - - ))} - -
    - Tranche de l'assiette - - {typeof unit === 'string' ? ( - unit - ) : ( - - {explanation.tranches[0].taux != null - ? 'Taux' - : 'Montant'} - - )} - - Résultat -
    - {/* nous avons remarqué que la notion de taux moyen pour un barème à 2 tranches est moins pertinent pour les règles de calcul des indépendants. Règle empirique à faire évoluer ! */} - {showValues && - !explanation.returnRate && - barèmeType === 'marginal' && - explanation.tranches.length > 2 && ( - <> - - Taux moyen :{' '} - - - - )} - {explanation.returnRate && ( -

    - Ce barème ne retourne que le taux. -

    - )} - - } - /> - ) -} - -let Tranche = ({ - tranche: { - 'en-dessous de': maxOnly, - 'au-dessus de': minOnly, - de: min, - à: max, - taux, - nodeValue, - montant - }, - multiplicateur, - tranchesUnit, - resultUnit, - returnRate, - trancheValue, - showValues, - language -}) => { - const trancheFormatter = value => ( - - ) - - let activated = trancheValue > 0 - return ( - - - {maxOnly ? ( - <> - En-dessous de {trancheFormatter(maxOnly)} - - ) : minOnly ? ( - <> - Au-dessus de {trancheFormatter(minOnly)} - - ) : ( - <> - De {trancheFormatter(min)} à{' '} - {trancheFormatter(max)} - - )} - - {taux != null ? makeJsx(taux) : makeJsx(montant)} - {showValues && !returnRate && taux != null && ( - - - - )} - - ) -} - -function TrancheFormatter({ - language, - tranchesUnit, - resultUnit, - multiplicateur, - value -}) { - const multiplicateurAcronym = multiplicateur?.explanation?.acronyme - if (!multiplicateurAcronym) { - return numberFormatter({ - language, - style: serializeUnit(tranchesUnit) === '€' ? 'currency' : undefined - })(value) - } else { - return ( - <> - {value}  - - {multiplicateurAcronym} - {' '} - - - ) - } -} - -//eslint-disable-next-line -export default barèmeType => ( - nodeValue, - explanation, - lazyEval = identity, - unit -) => diff --git a/source/engine/mecanismViews/Barème.tsx b/source/engine/mecanismViews/Barème.tsx new file mode 100644 index 000000000..c97316422 --- /dev/null +++ b/source/engine/mecanismViews/Barème.tsx @@ -0,0 +1,123 @@ +import classNames from 'classnames' +import React from 'react' +import { Trans } from 'react-i18next' +import { makeJsx } from '../evaluation' +import './Barème.css' +import { Node, NodeValuePointer } from './common' + +export default function Barème(nodeValue, explanation, _, unit) { + return ( + + + + {/* nous avons remarqué que la notion de taux moyen pour un barème à 2 tranches est moins pertinent pour les règles de calcul des indépendants. Règle empirique à faire évoluer ! */} + {nodeValue !== null && explanation.tranches.length > 2 && ( + <> + + Taux moyen :{' '} + + + + )} + + } + /> + ) +} + +export let BarèmeAttributes = ({ explanation }) => { + const multiplicateur = explanation.multiplicateur + return ( + <> +
  • + + assiette:{' '} + + {makeJsx(explanation.assiette)} +
  • + {multiplicateur && !multiplicateur.isDefault && ( +
  • + + multiplicateur:{' '} + + {makeJsx(multiplicateur)} +
  • + )} + + ) +} + +export const TrancheTable = ({ tranches, multiplicateur }) => { + const activeTranche = tranches.find(({ isActive }) => isActive) + return ( + + + + + {tranches[0].taux && ( + + )} + {(tranches[0].montant || activeTranche?.nodeValue != null) && ( + + )} + + + + {tranches.map((tranche, i) => ( + + ))} + +
    + Plafonds des tranches + + Taux + + Montant +
    + ) +} + +const Tranche = ({ tranche, multiplicateur }) => { + const isHighlighted = tranche.isActive + return ( + + + {tranche.plafond.nodeValue === Infinity ? ( + Au-delà du dernier plafond + ) : ( + <> + {makeJsx(tranche.plafond)} + {multiplicateur && !multiplicateur.isDefault && ( + <> + {' × '} + {makeJsx(multiplicateur)} + + )} + + )} + + {tranche.taux && {makeJsx(tranche.taux)}} + {(tranche.nodeValue != null || tranche.montant) && ( + + {tranche.montant ? ( + makeJsx(tranche.montant) + ) : ( + + )} + + )} + + ) +} diff --git a/source/engine/mecanismViews/BarèmeContinu.js b/source/engine/mecanismViews/BarèmeContinu.js deleted file mode 100644 index d21de5da2..000000000 --- a/source/engine/mecanismViews/BarèmeContinu.js +++ /dev/null @@ -1,106 +0,0 @@ -import { ShowValuesConsumer } from 'Components/rule/ShowValuesContext' -import RuleLink from 'Components/RuleLink' -import { formatValue } from 'Engine/format' -import { sortObjectByKeys } from 'Engine/mecanismViews/common' -import { serializeUnit } from 'Engine/units' -import React from 'react' -import { Trans, useTranslation } from 'react-i18next' -import { BarèmeAttributes } from './Barème' -import './Barème.css' -import { Node, NodeValuePointer } from './common' - -let Comp = function Barème({ nodeValue, explanation, unit }) { - return ( - - {showValues => ( - - - - - - - - - - - {sortObjectByKeys(explanation.points).map(([seuil, taux]) => ( - - - - - ))} - -
    - Seuil - - Taux -
    - - {taux}
    - {showValues && ( - - - Votre taux :{' '} - - - - )} - {explanation.returnRate && ( -

    - Ce barème ne retourne que le taux. -

    - )} - - } - /> - )} -
    - ) -} - -function SeuilFormatteur({ value, multiplicateur, unit }) { - const { language } = useTranslation().i18n - if (value === 0) { - return '0' - } else { - return ( - <> - {formatValue({ - value, - language - })}{' '} - {multiplicateur ? ( - <> -   - - {multiplicateur.acronyme} - - - - ) : ( - serializeUnit(unit) - )}{' '} - - ) - } -} - -//eslint-disable-next-line -export default (nodeValue, explanation, _, unit) => ( - -) diff --git a/source/engine/mecanismViews/Grille.tsx b/source/engine/mecanismViews/Grille.tsx new file mode 100644 index 000000000..d53434bbc --- /dev/null +++ b/source/engine/mecanismViews/Grille.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { BarèmeAttributes, TrancheTable } from './Barème' +import './Barème.css' +import { Node } from './common' + +export default function Grille(nodeValue, explanation, _, unit) { + return ( + + + + + } + /> + ) +} diff --git a/source/engine/mecanismViews/TauxProgressif.tsx b/source/engine/mecanismViews/TauxProgressif.tsx new file mode 100644 index 000000000..1458cb249 --- /dev/null +++ b/source/engine/mecanismViews/TauxProgressif.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { Trans } from 'react-i18next' +import { BarèmeAttributes, TrancheTable } from './Barème' +import './Barème.css' +import { Node, NodeValuePointer } from './common' + +export default function TauxProgressif(nodeValue, explanation, _, unit) { + return ( + + + + {nodeValue != null && ( + <> + + Taux calculé :{' '} + {' '} + + + )} + + } + /> + ) +} diff --git a/source/engine/mecanismViews/colors.ts b/source/engine/mecanismViews/colors.ts index d514c5965..b491b7c30 100644 --- a/source/engine/mecanismViews/colors.ts +++ b/source/engine/mecanismViews/colors.ts @@ -7,9 +7,9 @@ const colors = { 'toutes ces conditions': '#3498db', composantes: '#3498db', variations: '#FF9800', - complément: '#795548', + 'taux progressif': '#795548', barème: '#607D8B', - barèmeLinéaire: '#AD1457' + grille: '#AD1457' } export default (name: string) => colors[name] || '#34495e' diff --git a/source/engine/mecanismViews/common.tsx b/source/engine/mecanismViews/common.tsx index aafba8c2a..4b121d121 100644 --- a/source/engine/mecanismViews/common.tsx +++ b/source/engine/mecanismViews/common.tsx @@ -129,14 +129,19 @@ export function Leaf({ const sitePaths = useContext(SitePathsContext) const flatRules = useSelector(flatRulesSelector) let rule = findRuleByDottedName(flatRules, dottedName) - + const title = rule.title || capitalise0(name) return ( {dottedName && ( - {rule.title || capitalise0(name)} {filter} + {rule.acronyme ? ( + {rule.acronyme} + ) : ( + title + )}{' '} + {filter} {!isNil(nodeValue) && ( diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index f42a6fead..9110577eb 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -54,8 +54,8 @@ export let mecanismOneOf = (recurse, k, v) => { value={nodeValue} child={
      - {explanation.map(item => ( -
    • {makeJsx(item)}
    • + {explanation.map((item, i) => ( +
    • {makeJsx(item)}
    • ))}
    } @@ -105,8 +105,8 @@ export let mecanismAllOf = (recurse, k, v) => { value={nodeValue} child={
      - {explanation.map(item => ( -
    • {makeJsx(item)}
    • + {explanation.map((item, i) => ( +
    • {makeJsx(item)}
    • ))}
    } @@ -623,9 +623,6 @@ export let mecanismSynchronisation = (recurse, k, v) => { } } -export let mecanismError = (recurse, k, v) => { - throw new Error("Le mécanisme '" + k + "' est inconnu !" + v) -} export let mecanismOnePossibility = dottedName => (recurse, k, v) => ({ ...v, 'une possibilité': 'oui', diff --git a/source/engine/mecanisms/barème-continu.js b/source/engine/mecanisms/barème-continu.js deleted file mode 100644 index 29e22156c..000000000 --- a/source/engine/mecanisms/barème-continu.js +++ /dev/null @@ -1,82 +0,0 @@ -import { defaultNode, evaluateObject, parseObject } from 'Engine/evaluation' -import { decompose } from 'Engine/mecanisms/utils' -import variations from 'Engine/mecanisms/variations' -import BarèmeContinu from 'Engine/mecanismViews/BarèmeContinu' -import { anyNull, val } from 'Engine/traverse-common-functions' -import { parseUnit } from 'Engine/units' -import { - aperture, - isEmpty, - last, - pipe, - reduce, - reduced, - sort, - toPairs -} from 'ramda' -export default (recurse, k, v) => { - if (v.composantes) { - return decompose(recurse, k, v) - } - if (v.variations) { - return variations(recurse, k, v, true) - } - if (!v.points || typeof v.points !== 'object' || isEmpty(v.points)) { - throw new Error( - 'Le mécanisme `barème linéaire` doit avoir un paramètre `points` valide' - ) - } - let objectShape = { - assiette: false, - multiplicateur: defaultNode(1) - } - let returnRate = v['retourne seulement le taux'] === 'oui' - let effect = ({ assiette, multiplicateur, points }) => { - if (anyNull([assiette, multiplicateur])) return null - //We'll build a linear function given the two constraints that must be respected - let result = pipe( - toPairs, - // we don't rely on the sorting of objects - sort(([k1], [k2]) => k1 - k2), - points => [...points, [Infinity, last(points)[1]]], - aperture(2), - reduce((_, [[lowerLimit, lowerRate], [upperLimit, upperRate]]) => { - let x1 = val(multiplicateur) * lowerLimit, - x2 = val(multiplicateur) * upperLimit, - y1 = (val(assiette) * val(recurse(lowerRate))) / 100, - y2 = (val(assiette) * val(recurse(upperRate))) / 100 - if (val(assiette) > x1 && val(assiette) <= x2) { - // Outside of these 2 limits, it's a linear function a * x + b - let a = (y2 - y1) / (x2 - x1), - b = y1 - x1 * a, - nodeValue = a * val(assiette) + b, - taux = nodeValue / val(assiette) - return reduced({ - nodeValue: returnRate ? taux * 100 : nodeValue, - additionalExplanation: { - seuil: val(assiette) / val(multiplicateur), - taux, - unit: returnRate ? parseUnit('%') : assiette.unit - } - }) - } - }, 0) - )(points) - return result - } - let explanation = { - ...parseObject(recurse, objectShape, v), - points: v.points, - returnRate - }, - evaluate = evaluateObject(objectShape, effect) - return { - evaluate, - jsx: BarèmeContinu, - explanation, - category: 'mecanism', - name: 'barème continu', - type: 'numeric', - unit: returnRate ? parseUnit('%') : explanation.assiette.unit - } -} diff --git a/source/engine/mecanisms/barème-linéaire.js b/source/engine/mecanisms/barème-linéaire.js deleted file mode 100644 index 2b9486dc4..000000000 --- a/source/engine/mecanisms/barème-linéaire.js +++ /dev/null @@ -1,87 +0,0 @@ -import { defaultNode, evaluateObject, parseObject } from 'Engine/evaluation' -import { decompose } from 'Engine/mecanisms/utils' -import variations from 'Engine/mecanisms/variations' -import Barème from 'Engine/mecanismViews/Barème' -import { evaluateNode } from 'Engine/evaluation' -import { val } from 'Engine/traverse-common-functions' -import { parseUnit } from 'Engine/units' -import { desugarScale } from './barème' - -/* on réécrit en une syntaxe plus bas niveau mais plus régulière les tranches : - `en-dessous de: 1` - devient - ``` - de: 0 - à: 1 - ``` - */ - -export default (recurse, k, v) => { - if (v.composantes) { - //mécanisme de composantes. Voir known-mecanisms.md/composantes - return decompose(recurse, k, v) - } - if (v.variations) { - return variations(recurse, k, v, true) - } - - let returnRate = v['retourne seulement le taux'] === 'oui' - let tranches = desugarScale(recurse)(v['tranches']), - objectShape = { - assiette: false, - multiplicateur: defaultNode(1) - } - - let effect = ({ assiette, multiplicateur, tranches }, cache, situationGate, parsedRules) => { - if (val(assiette) === null) return null - - let roundedAssiette = Math.round(val(assiette)) - - let matchedTranche = tranches.find( - ({ de: min, à: max }) => - roundedAssiette >= val(multiplicateur) * min && - roundedAssiette <= max * val(multiplicateur) - ) - let nodeValue - if (!matchedTranche) { - nodeValue = 0 - } else if (matchedTranche.taux) { - nodeValue = returnRate - ? matchedTranche.taux.nodeValue - : (matchedTranche.taux.nodeValue / 100) * val(assiette) - } else { - matchedTranche.montant = evaluateNode(cache, situationGate, parsedRules, matchedTranche.montant) - nodeValue = matchedTranche.montant.nodeValue - } - - return { - nodeValue, - additionalExplanation: { - matchedTranche, - unit: returnRate - ? parseUnit('%') - : (v['unité'] && parseUnit(v['unité'])) || explanation.assiette.unit - } - } - } - - let explanation = { - ...parseObject(recurse, objectShape, v), - returnRate, - tranches - }, - evaluate = evaluateObject(objectShape, effect), - unit = returnRate - ? parseUnit('%') - : (v['unité'] && parseUnit(v['unité'])) || explanation.assiette.unit - return { - evaluate, - jsx: Barème('linéaire'), - explanation, - category: 'mecanism', - name: 'barème linéaire', - barème: 'en taux', - type: 'numeric', - unit - } -} diff --git a/source/engine/mecanisms/barème.js b/source/engine/mecanisms/barème.js deleted file mode 100644 index db1a86f32..000000000 --- a/source/engine/mecanisms/barème.js +++ /dev/null @@ -1,138 +0,0 @@ -import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation' -import { decompose } from 'Engine/mecanisms/utils' -import variations from 'Engine/mecanisms/variations' -import Barème from 'Engine/mecanismViews/Barème' -import { evolve, has } from 'ramda' -import { typeWarning } from '../error' -import { convertNodeToUnit } from '../nodeUnits' -import { parseUnit } from '../units' - -export let desugarScale = recurse => tranches => - tranches - .map(t => - has('en-dessous de')(t) - ? { ...t, de: 0, à: t['en-dessous de'] } - : has('au-dessus de')(t) - ? { ...t, de: t['au-dessus de'], à: Infinity } - : t - ) - .map(evolve({ taux: recurse, montant: recurse })) - -// This function was also used for marginal barèmes, but now only for linear ones -export let trancheValue = (assiette, multiplicateur) => ({ - de: min, - à: max, - taux, - montant -}) => - Math.round(assiette.nodeValue) >= min * multiplicateur.nodeValue && - (!max || Math.round(assiette.nodeValue) <= max * multiplicateur.nodeValue) - ? taux != null - ? assiette.nodeValue * taux.nodeValue - : montant - : 0 - -export default (recurse, k, v) => { - // Barème en taux marginaux. - - if (v.composantes) { - //mécanisme de composantes. Voir known-mecanisms.md/composantes - return decompose(recurse, k, v) - } - if (v.variations) { - return variations(recurse, k, v, true) - } - - let { assiette, multiplicateur } = v, - tranches = desugarScale(recurse)(v['tranches']) - - let explanation = { - assiette: recurse(assiette), - multiplicateur: multiplicateur ? recurse(multiplicateur) : defaultNode(1), - tranches - } - - let evaluate = (cache, situationGate, parsedRules, node) => { - let { assiette, multiplicateur } = node.explanation - assiette = evaluateNode(cache, situationGate, parsedRules, assiette) - multiplicateur = evaluateNode( - cache, - situationGate, - parsedRules, - multiplicateur - ) - try { - multiplicateur = convertNodeToUnit(assiette.unit, multiplicateur) - } catch (e) { - typeWarning( - cache._meta.contextRule, - `L'unité du multiplicateur du barème doit être compatible avec celle de son assiette`, - e - ) - } - const tranches = node.explanation.tranches.map(tranche => { - let { de: min, à: max, taux } = tranche - if ( - [assiette, multiplicateur].every( - ({ nodeValue }) => nodeValue != null - ) && - assiette.nodeValue < min * multiplicateur.nodeValue - ) { - return { ...tranche, nodeValue: 0 } - } - taux = convertNodeToUnit( - parseUnit(''), - evaluateNode(cache, situationGate, parsedRules, taux) - ) - if ( - [assiette, multiplicateur, taux].some( - ({ nodeValue }) => nodeValue == null - ) - ) { - return { - ...tranche, - nodeValue: null, - missingVariables: taux.missingVariables - } - } - return { - ...tranche, - nodeValue: - (Math.min(assiette.nodeValue, max * multiplicateur.nodeValue) - - min * multiplicateur.nodeValue) * - taux.nodeValue - } - }) - - const nodeValue = tranches.reduce( - (value, { nodeValue }) => (nodeValue == null ? null : value + nodeValue), - 0 - ) - const missingVariables = mergeAllMissing([ - assiette, - multiplicateur, - ...tranches - ]) - return { - ...node, - nodeValue, - explanation: { - ...explanation, - tranches - }, - missingVariables, - unit: assiette.unit, - lazyEval: node => evaluateNode(cache, situationGate, parsedRules, node) - } - } - - return { - explanation, - evaluate, - jsx: Barème('marginal'), - category: 'mecanism', - name: 'barème', - barème: 'marginal', - unit: explanation.assiette.unit - } -} diff --git a/source/engine/mecanisms/barème.ts b/source/engine/mecanisms/barème.ts new file mode 100644 index 000000000..2ebd8aa4e --- /dev/null +++ b/source/engine/mecanisms/barème.ts @@ -0,0 +1,91 @@ +import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation' +import { decompose } from 'Engine/mecanisms/utils' +import variations from 'Engine/mecanisms/variations' +import Barème from 'Engine/mecanismViews/Barème' +import { convertUnit, parseUnit } from '../units' +import { + evaluatePlafondUntilActiveTranche, + parseTranches +} from './trancheUtils' + +export default function parse(parse, k, v) { + // Barème en taux marginaux. + + if (v.composantes) { + //mécanisme de composantes. Voir known-mecanisms.md/composantes + return decompose(parse, k, v) + } + if (v.variations) { + return variations(parse, k, v, true) + } + const explanation = { + assiette: parse(v.assiette), + multiplicateur: v.multiplicateur ? parse(v.multiplicateur) : defaultNode(1), + tranches: parseTranches(parse, v.tranches) + } + return { + explanation, + evaluate, + jsx: Barème, + category: 'mecanism', + name: 'barème', + type: 'numeric', + unit: explanation.assiette.unit + } +} + +const evaluate = ( + cache, + situationGate, + parsedRules, + node: ReturnType +) => { + const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) + const assiette = evaluate(node.explanation.assiette) + const multiplicateur = evaluate(node.explanation.multiplicateur) + const tranches = evaluatePlafondUntilActiveTranche( + evaluate, + { + parsedTranches: node.explanation.tranches, + assiette, + multiplicateur + }, + cache + ).map(tranche => { + if (tranche.isAfterActive) { + return { ...tranche, nodeValue: 0 } + } + const taux = evaluate(tranche.taux) + if ([taux.nodeValue, tranche.nodeValue].some(value => value === null)) { + return { + ...tranche, + taux, + nodeValue: null, + missingVariables: mergeAllMissing([taux, tranche]) + } + } + return { + ...tranche, + taux, + unit: assiette.unit, + nodeValue: + (Math.min(assiette.nodeValue, tranche.plafondValue) - + tranche.plancherValue) * + convertUnit(taux.unit, parseUnit(''), taux.nodeValue) + } + }) + return { + ...node, + nodeValue: tranches.reduce( + (value, { nodeValue }) => (nodeValue == null ? null : value + nodeValue), + 0 + ), + missingVariables: mergeAllMissing(tranches), + explanation: { + assiette, + multiplicateur, + tranches + }, + unit: assiette.unit + } +} diff --git a/source/engine/mecanisms/grille.ts b/source/engine/mecanisms/grille.ts new file mode 100644 index 000000000..1fd819fdd --- /dev/null +++ b/source/engine/mecanisms/grille.ts @@ -0,0 +1,85 @@ +import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation' +import { decompose } from 'Engine/mecanisms/utils' +import variations from 'Engine/mecanisms/variations' +import grille from 'Engine/mecanismViews/Grille' +import { parseUnit } from 'Engine/units' +import { lensPath, over } from 'ramda' +import { + evaluatePlafondUntilActiveTranche, + parseTranches +} from './trancheUtils' + +export default function parse(parse, k, v) { + if (v.composantes) { + //mécanisme de composantes. Voir known-mecanisms.md/composantes + return decompose(parse, k, v) + } + if (v.variations) { + return variations(parse, k, v, true) + } + const defaultUnit = v['unité'] && parseUnit(v['unité']) + let explanation = { + assiette: parse(v.assiette), + multiplicateur: v.multiplicateur ? parse(v.multiplicateur) : defaultNode(1), + tranches: parseTranches(parse, v.tranches).map( + over(lensPath(['montant', 'unit']), unit => unit ?? defaultUnit) + ) + } + return { + explanation, + evaluate, + jsx: grille, + category: 'mecanism', + name: 'grille', + type: 'numeric', + unit: explanation.tranches[0].montant.unit + } +} + +const evaluate = ( + cache, + situationGate, + parsedRules, + node: ReturnType +) => { + const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) + const assiette = evaluate(node.explanation.assiette) + const multiplicateur = evaluate(node.explanation.multiplicateur) + const tranches = evaluatePlafondUntilActiveTranche( + evaluate, + { + parsedTranches: node.explanation.tranches, + assiette, + multiplicateur + }, + cache + ).map(tranche => { + if (tranche.isActive === false) { + return tranche + } + const montant = evaluate(tranche.montant) + return { + ...tranche, + montant, + nodeValue: montant.nodeValue, + unit: montant.unit, + missingVariables: mergeAllMissing([montant, tranche]) + } + }) + + const activeTranches = tranches.filter(({ isActive }) => isActive != false) + const missingVariables = mergeAllMissing(activeTranches) + const nodeValue = activeTranches.length ? activeTranches[0].nodeValue : false + + return { + ...node, + explanation: { + tranches, + assiette, + multiplicateur + }, + missingVariables, + nodeValue, + unit: activeTranches[0]?.unit ?? node.unit + } +} diff --git a/source/engine/mecanisms/tauxProgressif.ts b/source/engine/mecanisms/tauxProgressif.ts new file mode 100644 index 000000000..f873c43fd --- /dev/null +++ b/source/engine/mecanisms/tauxProgressif.ts @@ -0,0 +1,134 @@ +import { defaultNode, evaluateNode, mergeAllMissing } from 'Engine/evaluation' +import { decompose } from 'Engine/mecanisms/utils' +import variations from 'Engine/mecanisms/variations' +import tauxProgressif from 'Engine/mecanismViews/TauxProgressif' +import { convertNodeToUnit } from 'Engine/nodeUnits' +import { anyNull } from 'Engine/traverse-common-functions' +import { parseUnit } from 'Engine/units' +import { + evaluatePlafondUntilActiveTranche, + parseTranches +} from './trancheUtils' + +export default function parse(parse, k, v) { + if (v.composantes) { + //mécanisme de composantes. Voir known-mecanisms.md/composantes + return decompose(parse, k, v) + } + if (v.variations) { + return variations(parse, k, v, true) + } + + let explanation = { + assiette: parse(v.assiette), + multiplicateur: v.multiplicateur ? parse(v.multiplicateur) : defaultNode(1), + tranches: parseTranches(parse, v.tranches) + } + return { + evaluate, + jsx: tauxProgressif, + explanation, + category: 'mecanism', + name: 'taux progressif', + type: 'numeric', + unit: parseUnit('%') + } +} + +const evaluate = ( + cache, + situationGate, + parsedRules, + node: ReturnType +) => { + const evaluate = evaluateNode.bind(null, cache, situationGate, parsedRules) + const assiette = evaluate(node.explanation.assiette) + const multiplicateur = evaluate(node.explanation.multiplicateur) + const tranches = evaluatePlafondUntilActiveTranche( + evaluate, + { + parsedTranches: node.explanation.tranches, + assiette, + multiplicateur + }, + cache + ) + + const evaluatedNode = { + ...node, + explanation: { + tranches, + assiette, + multiplicateur + }, + unit: parseUnit('%') + } + + const lastTranche = tranches[tranches.length - 1] + if ( + tranches.every(({ isActive }) => isActive === false) || + (lastTranche.isActive && lastTranche.plafond.nodeValue === Infinity) + ) { + const taux = convertNodeToUnit(parseUnit('%'), evaluate(lastTranche.taux)) + const { nodeValue, missingVariables } = taux + lastTranche.taux = taux + lastTranche.nodeValue = nodeValue + lastTranche.missingVariables = missingVariables + return { + ...evaluatedNode, + nodeValue, + missingVariables + } + } + + if (tranches.every(({ isActive }) => isActive !== true)) { + return { + ...evaluatedNode, + nodeValue: null, + missingVariables: mergeAllMissing(tranches) + } + } + + const activeTrancheIndex = tranches.findIndex( + ({ isActive }) => isActive === true + ) + const activeTranche = tranches[activeTrancheIndex] + activeTranche.taux = convertNodeToUnit( + parseUnit('%'), + evaluate(activeTranche.taux) + ) + + const previousTranche = tranches[activeTrancheIndex - 1] + if (previousTranche) { + previousTranche.taux = convertNodeToUnit( + parseUnit('%'), + evaluate(previousTranche.taux) + ) + previousTranche.isActive = true + } + const previousTaux = previousTranche + ? previousTranche.taux + : activeTranche.taux + const calculationValues = [previousTaux, activeTranche.taux, activeTranche] + if (anyNull(calculationValues)) { + activeTranche.nodeValue = null + activeTranche.missingVariables = mergeAllMissing(calculationValues) + return { + ...evaluatedNode, + nodeValue: null, + activeTranche: activeTranche.missingVariables + } + } + + const lowerTaux = previousTaux.nodeValue + const upperTaux = activeTranche.taux.nodeValue + const plancher = activeTranche.plancherValue + const plafond = activeTranche.plafondValue + const coeff = (upperTaux - lowerTaux) / (plafond - plancher) + const nodeValue = lowerTaux + (assiette.nodeValue - plancher) * coeff + activeTranche.nodeValue = nodeValue + return { + ...evaluatedNode, + nodeValue + } +} diff --git a/source/engine/mecanisms/trancheUtils.ts b/source/engine/mecanisms/trancheUtils.ts new file mode 100644 index 000000000..f0e493316 --- /dev/null +++ b/source/engine/mecanisms/trancheUtils.ts @@ -0,0 +1,97 @@ +import { mergeAllMissing } from 'Engine/evaluation' +import { evolve } from 'ramda' +import { evaluationError, typeWarning } from '../error' +import { convertUnit, inferUnit } from '../units' + +export const parseTranches = (parse, tranches) => { + return tranches + .map((t, i) => { + if (!t.plafond && i > tranches.length) { + console.log(t, i) + throw new SyntaxError( + `La tranche n°${i} du barème n'a pas de plafond précisé. Seule la dernière tranche peut ne pas être plafonnée` + ) + } + return { ...t, plafond: t.plafond ?? Infinity } + }) + .map(evolve({ taux: parse, montant: parse, plafond: parse })) +} + +export function evaluatePlafondUntilActiveTranche( + evaluate, + { multiplicateur, assiette, parsedTranches }, + cache +) { + return parsedTranches.reduce( + ([tranches, activeTrancheFound], parsedTranche, i: number) => { + if (activeTrancheFound) { + return [ + [...tranches, { ...parsedTranche, isAfterActive: true }], + activeTrancheFound + ] + } + + const plafond = evaluate(parsedTranche.plafond) + const plancher = tranches[i - 1] + ? tranches[i - 1].plafond + : { nodeValue: 0 } + const calculationValues = [plafond, assiette, multiplicateur, plancher] + if (calculationValues.some(node => node.nodeValue === null)) { + return [ + [ + ...tranches, + { + ...parsedTranche, + plafond, + nodeValue: null, + isActive: null, + isAfterActive: false, + missingVariables: mergeAllMissing(calculationValues) + } + ], + false + ] + } + let plafondValue = plafond.nodeValue * multiplicateur.nodeValue + try { + plafondValue = [Infinity || 0].includes(plafondValue) + ? plafondValue + : convertUnit( + inferUnit('*', [plafond.unit, multiplicateur.unit]), + assiette.unit, + plafondValue + ) + } catch (e) { + typeWarning( + cache._meta.contextRule, + `L'unité du plafond de la tranche n°${i + + 1} n'est pas compatible avec celle l'assiette`, + e + ) + } + + let plancherValue = tranches[i - 1] ? tranches[i - 1].plafondValue : 0 + if (!!tranches[i - 1] && plafondValue <= plancherValue) { + evaluationError( + cache._meta.contextRule, + `Le plafond de la tranche n°${i + + 1} a une valeur inférieure à celui de la tranche précédente` + ) + } + + const tranche = { + ...parsedTranche, + plafond, + plancherValue, + plafondValue, + isAfterActive: false, + isActive: + assiette.nodeValue >= plancherValue && + assiette.nodeValue < plafondValue + } + + return [[...tranches, tranche], tranche.isActive] + }, + [[], false] + )[0] +} diff --git a/source/engine/parse.js b/source/engine/parse.js index bdafff5cc..9d3f5d8ee 100644 --- a/source/engine/parse.js +++ b/source/engine/parse.js @@ -5,38 +5,30 @@ import { formatValue } from 'Engine/format' import mecanismRound from 'Engine/mecanisms/arrondi' import barème from 'Engine/mecanisms/barème' -import barèmeContinu from 'Engine/mecanisms/barème-continu' -import barèmeLinéaire from 'Engine/mecanisms/barème-linéaire' import durée from 'Engine/mecanisms/durée' import encadrement from 'Engine/mecanisms/encadrement' +import grille from 'Engine/mecanisms/grille' import operation from 'Engine/mecanisms/operation' +import tauxProgressif from 'Engine/mecanisms/tauxProgressif' import variations from 'Engine/mecanisms/variations' import { Grammar, Parser } from 'nearley' import { add, - cond, divide, equals, fromPairs, gt, gte, - is, - keys, lt, lte, multiply, - propOr, - subtract, - T, - without + subtract } from 'ramda' import React from 'react' import { syntaxError } from './error.ts' import grammar from './grammar.ne' import { mecanismAllOf, - mecanismComplement, - mecanismError, mecanismInversion, mecanismMax, mecanismMin, @@ -49,154 +41,156 @@ import { } from './mecanisms' import { parseReferenceTransforms } from './parseReference' -export let parse = (rules, rule, parsedRules) => rawNode => { - let onNodeType = cond([ - [is(String), parseString(rules, rule, parsedRules)], - [is(Number), parseNumber], - [is(Object), parseObject(rules, rule, parsedRules)], - [T, parseOther] - ]) +export const parse = (rules, rule, parsedRules) => rawNode => { + if (rawNode == null) { + syntaxError( + rule.dottedName, + ` +Une des valeurs de la formule est vide. +Vérifiez que tous les champs à droite des deux points sont remplis` + ) + } + if (typeof rawNode === 'boolean') { + syntaxError( + rule.dottedName, + ` +Les valeure booléenes true / false ne sont acceptée. +Utilisez leur contrepartie française : 'oui' / 'non'` + ) + } + const node = + typeof rawNode === 'object' ? rawNode : parseExpression(rule, '' + rawNode) - let defaultEvaluate = (cache, situationGate, parsedRules, node) => node - let parsedNode = onNodeType(rawNode) - - return parsedNode.evaluate - ? parsedNode - : { ...parsedNode, evaluate: defaultEvaluate } + const parsedNode = parseMecanism(rules, rule, parsedRules)(node) + parsedNode.evaluate = parsedNode.evaluate ?? ((_, __, ___, node) => node) + return parsedNode } const compiledGrammar = Grammar.fromCompiled(grammar) -export let parseString = (rules, rule, parsedRules) => rawNode => { +const parseExpression = (rule, rawNode) => { /* Strings correspond to infix expressions. * Indeed, a subset of expressions like simple arithmetic operations `3 + (quantity * 2)` or like `salary [month]` are more explicit that their prefixed counterparts. * This function makes them prefixed operations. */ try { let [parseResult] = new Parser(compiledGrammar).feed(rawNode).results - return parseObject(rules, rule, parsedRules)(parseResult) + return parseResult } catch (e) { syntaxError( rule.dottedName, - `\`${rawNode}\` n'est pas une formule valide`, + `\`${rawNode}\` n'est pas une expression valide`, e ) } } -export let parseNumber = rawNode => ({ - text: '' + rawNode, - category: 'number', - nodeValue: rawNode, - type: 'numeric', - jsx: {rawNode} -}) - -export let parseOther = rawNode => { - throw new Error( - 'Cette donnée : ' + rawNode + ' doit être un Number, String ou Object' - ) -} - -export let parseObject = (rules, rule, parsedRules) => rawNode => { - /* TODO instead of describing mecanisms in knownMecanisms.yaml, externalize the mecanisms themselves in an individual file and describe it - let mecanisms = intersection(keys(rawNode), keys(knownMecanisms)) - - if (mecanisms.length != 1) { - } - */ - - let attributes = keys(rawNode), - descriptiveAttributes = ['description', 'note', 'référence'], - relevantAttributes = without(descriptiveAttributes, attributes) - if (relevantAttributes.length !== 1) - throw new Error(`OUPS : On ne devrait reconnaître que un et un seul mécanisme dans cet objet (au-delà des attributs descriptifs tels que "description", "commentaire", etc.) - Objet YAML : ${JSON.stringify(rawNode)} - Cette liste doit avoir un et un seul élément. - Si vous venez tout juste d'ajouter un nouveau mécanisme, vérifier qu'il est bien intégré dans le dispatch de parse.js - `) - let k = relevantAttributes[0], - v = rawNode[k] - - let knownOperations = { - '*': [multiply, '×'], - '/': [divide, '∕'], - '+': [add], - '-': [subtract, '−'], - '<': [lt], - '<=': [lte, '≤'], - '>': [gt], - '>=': [gte, '≥'], - '=': [equals], - '!=': [(a, b) => !equals(a, b), '≠'] - }, - operationDispatch = fromPairs( - Object.entries(knownOperations).map(([k, [f, symbol]]) => [ - k, - operation(k, f, symbol) - ]) +const parseMecanism = (rules, rule, parsedRules) => rawNode => { + if (Object.keys(rawNode).length > 1) { + syntaxError( + rule.dottedName, + ` +Les mécanismes suivants se situent au même niveau : ${Object.keys(rawNode) + .map(x => `'${x}'`) + .join(', ')} +Cela vient probablement d'une erreur dans l'indentation + ` ) + } + const mecanismName = Object.keys(rawNode)[0] + const values = rawNode[mecanismName] - let dispatch = { - 'une de ces conditions': mecanismOneOf, - 'toutes ces conditions': mecanismAllOf, - somme: mecanismSum, - multiplication: mecanismProduct, - arrondi: mecanismRound, - barème, - 'barème linéaire': barèmeLinéaire, - 'barème continu': barèmeContinu, - encadrement, - durée, - 'le maximum de': mecanismMax, - 'le minimum de': mecanismMin, - complément: mecanismComplement, - 'une possibilité': mecanismOnePossibility(rule.dottedName), - 'inversion numérique': mecanismInversion(rule.dottedName), - allègement: mecanismReduction, - variations, - synchronisation: mecanismSynchronisation, - ...operationDispatch, - filter: () => - parseReferenceTransforms( - rules, - rule, - parsedRules - )({ - filter: v.filter, - variable: v.explanation - }), - variable: () => - parseReferenceTransforms(rules, rule, parsedRules)({ variable: v }), - unitConversion: () => - parseReferenceTransforms( - rules, - rule, - parsedRules - )({ - variable: v.explanation, - unit: v.unit - }), - constant: () => ({ - type: v.type, - nodeValue: v.nodeValue, - unit: v.unit, - // eslint-disable-next-line - jsx: () => ( - - {formatValue({ - unit: v.unit, - value: v.nodeValue, - // TODO : handle localization here - language: 'fr', - // We want to display constants with full precision, - // espacilly for percentages like APEC 0,036 % - maximumFractionDigits: 5 - })} - - ) + const parseFunctions = { + ...statelessParseFunction, + 'une possibilité': mecanismOnePossibility(rule.dottedName), + 'inversion numérique': mecanismInversion(rule.dottedName), + filter: () => + parseReferenceTransforms( + rules, + rule, + parsedRules + )({ + filter: values.filter, + variable: values.explanation + }), + variable: () => + parseReferenceTransforms(rules, rule, parsedRules)({ variable: values }), + unitConversion: () => + parseReferenceTransforms( + rules, + rule, + parsedRules + )({ + variable: values.explanation, + unit: values.unit }) - }, - action = propOr(mecanismError, k, dispatch) + } - return action(parse(rules, rule, parsedRules), k, v) + const parseFn = parseFunctions[mecanismName] + if (!parseFn) { + syntaxError( + rule.dottedName, + ` +Le mécanisme ${mecanismName} est inconnu. +Vérifiez qu'il n'y ait pas d'erreur dans l'orthographe du nom.` + ) + } + return parseFn(parse(rules, rule, parsedRules), mecanismName, values) +} + +const knownOperations = { + '*': [multiply, '×'], + '/': [divide, '∕'], + '+': [add], + '-': [subtract, '−'], + '<': [lt], + '<=': [lte, '≤'], + '>': [gt], + '>=': [gte, '≥'], + '=': [equals], + '!=': [(a, b) => !equals(a, b), '≠'] +} + +const operationDispatch = fromPairs( + Object.entries(knownOperations).map(([k, [f, symbol]]) => [ + k, + operation(k, f, symbol) + ]) +) + +const statelessParseFunction = { + ...operationDispatch, + 'une de ces conditions': mecanismOneOf, + 'toutes ces conditions': mecanismAllOf, + somme: mecanismSum, + multiplication: mecanismProduct, + arrondi: mecanismRound, + barème, + grille, + 'taux progressif': tauxProgressif, + encadrement, + durée, + 'le maximum de': mecanismMax, + 'le minimum de': mecanismMin, + allègement: mecanismReduction, + variations, + synchronisation: mecanismSynchronisation, + constant: (_, __, v) => ({ + type: v.type, + nodeValue: v.nodeValue, + unit: v.unit, + // eslint-disable-next-line + jsx: (nodeValue, _, __, unit) => ( + + {formatValue({ + unit: unit, + value: nodeValue, + // TODO : handle localization here + language: 'fr', + // We want to display constants with full precision, + // espacilly for percentages like APEC 0,036 % + maximumFractionDigits: 5 + })} + + ) + }) } diff --git a/source/engine/units.ts b/source/engine/units.ts index 74f365810..0d40601c9 100644 --- a/source/engine/units.ts +++ b/source/engine/units.ts @@ -50,7 +50,7 @@ let printUnits = (units: Array, count: number, lng): string => const plural = 2 export let serializeUnit = ( - rawUnit: Unit | null | string, + rawUnit: Unit | undefined | string, count: number = plural, lng: string = 'fr' ) => { @@ -88,7 +88,7 @@ type SupportedOperators = '*' | '/' | '+' | '-' let noUnit = { numerators: [], denominators: [] } export let inferUnit = ( operator: SupportedOperators, - rawUnits: Array + rawUnits: Array ): Unit | undefined => { let units = rawUnits.map(u => u || noUnit) if (operator === '*') @@ -182,7 +182,11 @@ function unitsConversionFactor(from: string[], to: string[]): number { return factor } -export function convertUnit(from: Unit, to: Unit, value: number) { +export function convertUnit( + from: Unit | undefined, + to: Unit | undefined, + value: number +) { if (!areUnitConvertible(from, to)) { throw new Error( `Impossible de convertir l'unité '${serializeUnit( @@ -193,8 +197,8 @@ export function convertUnit(from: Unit, to: Unit, value: number) { if (!value) { return value } - const [fromSimplified, factorTo] = simplifyUnitWithValue(from) - const [toSimplified, factorFrom] = simplifyUnitWithValue(to) + const [fromSimplified, factorTo] = simplifyUnitWithValue(from || noUnit) + const [toSimplified, factorFrom] = simplifyUnitWithValue(to || noUnit) return round( ((value * factorTo) / factorFrom) * unitsConversionFactor( @@ -239,7 +243,7 @@ export function simplifyUnitWithValue( value ? round(value * factor) : value ] } -export function areUnitConvertible(a: Unit, b: Unit) { +export function areUnitConvertible(a: Unit | undefined, b: Unit | undefined) { if (a == null || b == null) { return true } diff --git a/source/locales/en.yaml b/source/locales/en.yaml index 2b770dbb3..e73ff2898 100644 --- a/source/locales/en.yaml +++ b/source/locales/en.yaml @@ -7,6 +7,7 @@ Accueil: Home Alors: Then Année d'activité: Years of activity Assimilé salarié: '"Assimilé-salarié"' +Au-delà du dernier plafond: Beyond the last ceiling Au-dessus de: Above Aucun résultat: No result$ Auto-entrepreneur: Auto-entrepreneur @@ -62,6 +63,7 @@ Modifier: Modify Modifier mes réponses: Change my answers Mon entreprise: My company Mon revenu: My income +Montant: Amount Montant des cotisations: Amount of contributions 'Nom de l''entreprise ou SIREN ': Company name or SIREN code Non: 'No' @@ -76,6 +78,7 @@ Pas en auto-entrepreneur: Not in auto-entrepreneur Pas implémenté: Not implemented Passer: Skip Personnalisez l'integration: Customize the integration +Plafonds des tranches: Wafer ceilings Plein écran: Fullscreen Plus d'informations: More information (fr) Plusieurs associés: Several partners @@ -115,6 +118,7 @@ Simulations personnalisées: Customized simulations Sinon: Else Suivant: Next Taux: Rate +Taux calculé: Calculated rate Taux moyen: Average rate Total des retenues: Total withheld Tout effacer: Delete all diff --git a/source/locales/rules-en.yaml b/source/locales/rules-en.yaml index 3e8ec57f0..eaa827e1f 100644 --- a/source/locales/rules-en.yaml +++ b/source/locales/rules-en.yaml @@ -248,42 +248,76 @@ contrat salarié . CDD . CPF: titre.fr: CPF contrat salarié . CDD . compensation pour congés non pris: description.en: >- - The employee on a fixed-term contract has the same rights for paid leave as - the employee on a permanent contract. He acquires and takes his paid leave - under the same conditions. + [automatic] The employee with a fixed-term contract has the same paid + vacation entitlements as the employee with a permanent contract. He acquires + and takes his paid leave under the same conditions. + However, it is common that the employee may not take all of his or her leave + before the end of the contract, in which case he or she will receive a + compensatory allowance for paid leave paid by the employer. - It is however common that the employee can not take all his leave before the - end of his contract. In that case, he receives a compensatory alowance paid - by the employer. - description.fr: > + There are two methods of calculating the allowance for leave without pay. + + ### Tenths method + + This method of calculation will most often be favourable to the employee + when he or she has worked overtime. An allowance equal to one tenth of the + total gross remuneration received by the employee during the reference + period. + + ## Salary continuance method ## + + This method will most often be favourable to the employee when he or she has + received a salary increase. + + In making the calculation, the employer may take into account either : - the + actual time of the month, - the average number of working days (or working + days), - the actual number of working days (or working days). + description.fr: >- Le salarié en CDD bénéficie des mêmes droits à congés payés que le salarié en CDI. Il acquiert et prend ses congés payés dans les mêmes conditions. - Il est cependant courant que le salarié ne puisse pas prendre tous ses congés avant le terme de son contrat, il bénéficie alors d'une indemnité compensatrice de congés payés versée par l'employeur. + + Il existe deux méthodes pour calculer l'indemnité de congés non pris. + + ### Méthode "du dixième" + + Ce mode de calcul sera le plus souvent favorable au salarié lorsque celui-ci + a accompli des heures supplémentaires. Une indemnité égale au dixième de la + rémunération brute totale perçue par le salarié au cours de la période de + référence. + + ### Méthode "maintien du salaire" + + Cette méthode sera le plus souvent favorable au salarié lorsque celui-ci a + bénéficié d’une augmentation de salaire. + + Pour effectuer le calcul, l'employeur peut tenir compte soit : - de + l'horaire réel du mois, - du nombre moyen de jours ouvrables (ou ouvrés), - + du nombre réel de jours ouvrables (ou ouvrés). note.en: > [automatic] The indemnity is paid at the end of the contract, unless the fixed-term contract is continued by a permanent contract. Note that the El Khomri law modifies article L3141-12: - - before: Leave can be taken as soon as you become entitled.... + - before: Leave can be taken as soon as the entitlement arises. - - now: Leaves can be taken as soon as you are hired... + - now: Leaves can be taken as soon as you are hired. note.fr: > L'indemnité est versée à la fin du contrat, sauf si le CDD se poursuit par un CDI. À noter, la loi El Khomri modifie l'article L3141-12: - - avant : Les congés peuvent être pris dès l'ouverture des droits [...] + - avant : Les congés peuvent être pris dès l'ouverture des droits - - maintenant : Les congés peuvent être pris dès l’embauche [...] - titre.en: untaken vacation compensation - titre.fr: compensation pour congés non pris + - maintenant : Les congés peuvent être pris dès l’embauche + titre.en: '[automatic] holiday pay' + titre.fr: indemnité de congés payés contrat salarié . CDD . compensation pour congés non pris . assiette mensuelle: titre.en: monthly basis titre.fr: assiette mensuelle @@ -3223,6 +3257,9 @@ dirigeant . indépendant . PLNR régime général: question.fr: Avez-vous opté pour le rattachement au régime général des indépendants ? titre.en: PLNR general scheme titre.fr: PLNR régime général +dirigeant . indépendant . assiette des cotisations: + titre.en: '[automatic] contribution base' + titre.fr: assiette des cotisations dirigeant . indépendant . conjoint collaborateur: description.en: > [automatic] Allows the executive's spouse to be covered by social protection @@ -3309,12 +3346,12 @@ dirigeant . indépendant . conjoint collaborateur . assiette . revenu avec parta dirigeant . indépendant . conjoint collaborateur . assiette . revenu sans partage: description.en: >- [automatic] The collaborating spouse will pay social security contributions - calculated on the basis of a percentage of the professional income of the - manager of the company (one third or one half). + calculated on the basis of a percentage of the contribution base of the + company manager (one third or one half). description.fr: >- Le conjoint collaborateur paiera des cotisations sociales calculées sur une - base d'un pourcentage du revenu professionnel du gérant de l'entreprise (un - tiers ou la moitié). + base d'un pourcentage du assiette des cotisations du gérant de l'entreprise + (un tiers ou la moitié). titre.en: undivided income titre.fr: revenu sans partage dirigeant . indépendant . conjoint collaborateur . cotisations: @@ -3384,8 +3421,8 @@ dirigeant . indépendant . cotisations et contributions . cotisations . déducti titre.en: tobacco deduction titre.fr: déduction tabac dirigeant . indépendant . cotisations et contributions . cotisations . déduction tabac . revenus déduits: - titre.en: professional income (with tobacco deduction) - titre.fr: revenu professionnel (avec déduction tabac) + titre.en: '[automatic] contribution base (with tobacco deduction)' + titre.fr: assiette des cotisations (avec déduction tabac) dirigeant . indépendant . cotisations et contributions . cotisations . indemnités journalières maladie: description.en: >- Contributions for the daily allowances of self-employed people. If the state @@ -3515,6 +3552,9 @@ dirigeant . indépendant . cotisations et contributions . cotisations . maladie dirigeant . indépendant . cotisations et contributions . cotisations . retraite complémentaire: titre.en: supplementary pension titre.fr: retraite complémentaire +dirigeant . indépendant . cotisations et contributions . cotisations . retraite complémentaire . plafond: + titre.en: '[automatic] supplementary pension ceiling for self-employed persons' + titre.fr: plafond retraite complémentaire des indépendants dirigeant . indépendant . cotisations et contributions . cotisations . retraite complémentaire . taux spécifique PLNR: description.en: > Non-regulated liberal professions who started their activity as of 1 January @@ -3634,36 +3674,8 @@ dirigeant . indépendant . revenu net de cotisations: titre.en: net contribution income titre.fr: revenu net de cotisations dirigeant . indépendant . revenu professionnel: - description.en: > - It is the net income of the self-employed person from deductible - contributions that is used as the basis for calculating contributions and - taxes for the self-employed. - - Attention, **our calculation is valid for a standard scheme**: - - The self-employed person who starts out will pay a fixed contributions - during his first months. He will then have to regularize this situation in - relation to the income he actually received. - - This calculation should therefore be seen as *the amount that should in any - case be paid* in the short term after several months of activity. - description.fr: > - C'est le revenu net de cotisations déductibles du travailleur indépendant, - qui sert de base au calcul des cotisations et de l'impôt pour les - indépendants. - - - Attention, **notre calcul est fait au régime de croisière**: - - l'indépendant qui se lance paiera pendant ses 2 premières années un forfait - relativement réduit de cotisations sociales. Il devra ensuite régulariser - cette situation par rapport au revenu qu'il a vraiment perçu. - - - Il faut donc voir ce calcul comme *le montant qui devra de toute façon être - payé* à court terme après 2 ans d'exercice. - titre.en: Professional income - titre.fr: revenu professionnel (net imposable) + titre.en: '[automatic] occupational income' + titre.fr: revenu professionnel dirigeant . indépendant . revenus étrangers: description.en: > [automatic] Foreign income is income declared by self-employed persons in diff --git a/source/sites/publi.codes/exemple1.yaml b/source/sites/publi.codes/exemple1.yaml index b8f2eccb5..d6bfa817f 100644 --- a/source/sites/publi.codes/exemple1.yaml +++ b/source/sites/publi.codes/exemple1.yaml @@ -1,5 +1,5 @@ revenu imposable: - format: euros + unité: € revenu abattu: formule: @@ -12,25 +12,20 @@ impôt sur le revenu: barème: assiette: revenu abattu tranches: - - en-dessous de: 9807 - taux: 0% - - de: 9807 - à: 27086 - taux: 14% - - de: 27086 - à: 72617 - taux: 30% - - de: 72617 - à: 153783 - taux: 41% - - au-dessus de: 153783 - taux: 45% - + - taux: 0% + plafond: 9807€ + - taux: 14% + plafond: 27086€ + - taux: 30% + plafond: 72617€ + - taux: 41% + plafond: 153783€ + - taux: 45% + impôt final: formule: allègement: assiette: impôt sur le revenu décote: - plafond: 1177 taux: 75% - + plafond: 1177€ diff --git a/test/conversation.test.js b/test/conversation.test.js index b2476c558..a39684f27 100644 --- a/test/conversation.test.js +++ b/test/conversation.test.js @@ -18,8 +18,8 @@ describe('conversation', function() { { nom: 'top . startHere', formule: { somme: ['a', 'b'] } }, { nom: 'top . a', formule: 'aa' }, { nom: 'top . b', formule: 'bb' }, - { nom: 'top . aa', question: '?', titre: 'a' }, - { nom: 'top . bb', question: '?', titre: 'b' } + { nom: 'top . aa', question: '?', titre: 'a', unité: '€' }, + { nom: 'top . bb', question: '?', titre: 'b', unité: '€' } ], rules = rawRules.map(enrichRule), state = merge(baseState, { @@ -40,9 +40,9 @@ describe('conversation', function() { { nom: 'top . a', formule: 'aa' }, { nom: 'top . b', formule: 'bb' }, { nom: 'top . c', formule: 'cc' }, - { nom: 'top . aa', question: '?', titre: 'a' }, - { nom: 'top . bb', question: '?', titre: 'b' }, - { nom: 'top . cc', question: '?', titre: 'c' } + { nom: 'top . aa', question: '?', titre: 'a', unité: '€' }, + { nom: 'top . bb', question: '?', titre: 'b', unité: '€' }, + { nom: 'top . cc', question: '?', titre: 'c', unité: '€' } ], rules = rawRules.map(enrichRule) diff --git a/test/generateQuestions.test.js b/test/generateQuestions.test.js index 1743e3662..4053b7b62 100644 --- a/test/generateQuestions.test.js +++ b/test/generateQuestions.test.js @@ -159,9 +159,9 @@ describe('collectMissingVariables', function() { alors: { multiplicateur: 'deux', tranches: [ - { 'en-dessous de': 1, taux: 0.1 }, - { de: 1, à: 2, taux: 'trois' }, - { 'au-dessus de': 2, taux: 10 } + { plafond: 1, taux: 0.1 }, + { plafond: 2, taux: 'trois' }, + { taux: 10 } ] } }, @@ -170,8 +170,8 @@ describe('collectMissingVariables', function() { alors: { multiplicateur: 'quatre', tranches: [ - { 'en-dessous de': 1, taux: 0.1 }, - { de: 1, à: 2, taux: 1.8 }, + { plafond: 1, taux: 0.1 }, + { plafond: 2, taux: 1.8 }, { 'au-dessus de': 2, taux: 10 } ] } diff --git a/test/library.test.js b/test/library.test.js index 581c695a3..ccf79e75c 100644 --- a/test/library.test.js +++ b/test/library.test.js @@ -92,19 +92,15 @@ ya: barème: assiette: revenu abattu tranches: - - en-dessous de: 9807 - taux: 0% - - de: 9807 - à: 27086 - taux: 14% - - de: 27086 - à: 72617 - taux: 30% - - de: 72617 - à: 153783 - taux: 41% - - au-dessus de: 153783 - taux: 45% + - taux: 0% + plafond: 9807 + - taux: 14% + plafond: 27086 + - taux: 30% + plafond: 72617 + - taux: 41% + plafond: 153783 + - taux: 45% - nom: impôt sur le revenu à payer @@ -112,8 +108,8 @@ ya: allègement: assiette: impôt sur le revenu décote: - plafond: 1177 taux: 75% + plafond: 1177 ` let target = 'impôt sur le revenu à payer' diff --git a/test/mecanisms.test.js b/test/mecanisms.test.js index 463d70d3f..3413c4d35 100644 --- a/test/mecanisms.test.js +++ b/test/mecanisms.test.js @@ -6,10 +6,10 @@ */ import { expect } from 'chai' -import { serializeUnit } from 'Engine/units' import { collectMissingVariables } from '../source/engine/generateQuestions' import { enrichRule } from '../source/engine/rules' import { analyse, parseAll } from '../source/engine/traverse' +import { parseUnit } from '../source/engine/units' import testSuites from './load-mecanism-tests' describe('Mécanismes', () => @@ -59,7 +59,7 @@ describe('Mécanismes', () => if (unit) { expect(target.unit).not.to.be.equal(undefined) - expect(serializeUnit(target.unit)).to.eql(unit) + expect(target.unit).to.deep.equal(parseUnit(unit)) } }) )) diff --git a/test/mécanismes/allègement.yaml b/test/mécanismes/allègement.yaml index a8a661583..490bb11ea 100644 --- a/test/mécanismes/allègement.yaml +++ b/test/mécanismes/allègement.yaml @@ -23,8 +23,8 @@ montant décoté: allègement: assiette: montant décote: - plafond: 2040 taux: 100% + plafond: 2040 exemples: - situation: montant: 1000 @@ -37,8 +37,8 @@ montant franchisé et décoté: assiette: montant franchise: 1200 décote: - plafond: 2040 taux: 75% + plafond: 2040 exemples: - situation: montant: 100 @@ -103,8 +103,8 @@ montant franchisé, décote, abattu: assiette: montant franchise: 1200 décote: - plafond: 2040 taux: 75% + plafond: 2040 abattement: 20507 exemples: - situation: diff --git a/test/mécanismes/barème-continu.yaml b/test/mécanismes/barème-continu.yaml deleted file mode 100644 index c914c8a9a..000000000 --- a/test/mécanismes/barème-continu.yaml +++ /dev/null @@ -1,79 +0,0 @@ -base: - unité: £ - formule: 300 - -assiette: - unité: £ - -Simple: - formule: - barème continu: - assiette: assiette - multiplicateur: base - points: - 0: 0% - 0.4: 3.16% - 1.1: 6.35% - unité attendue: £ - exemples: - - nom: Premier point - situation: - assiette: 10 - valeur attendue: 0.026 - - nom: Deuxième point - situation: - assiette: 120 - valeur attendue: 3.792 - - nom: Premier point - situation: - assiette: 150 - valeur attendue: 5.423 - - nom: Troisième point - situation: - assiette: 330 - valeur attendue: 20.955 - - nom: Au-delà - situation: - assiette: 1000 - valeur attendue: 63.5 - -base deux: - unité: µ - formule: 300 - -assiette deux: - unité: µ - -Retour de taux, pas d'assiette: - unité: '%' - formule: - barème continu: - assiette: assiette deux - multiplicateur: base deux - points: - 0: 100% - 0.75: 100% - 1: 0% - retourne seulement le taux: oui - unité attendue: '%' - exemples: - - nom: Premier point - situation: - assiette deux: 200 - valeur attendue: 100 - - nom: Deuxième point - situation: - assiette deux: 225 - valeur attendue: 100 - - nom: Troisième point - situation: - assiette deux: 262.5 - valeur attendue: 50 - - nom: Quatrième point - situation: - assiette deux: 300 - valeur attendue: 0 - - nom: Cinquième point - situation: - assiette deux: 300 - valeur attendue: 0 diff --git a/test/mécanismes/barème-linéaire.yaml b/test/mécanismes/barème-linéaire.yaml deleted file mode 100644 index bccca4ad0..000000000 --- a/test/mécanismes/barème-linéaire.yaml +++ /dev/null @@ -1,76 +0,0 @@ -assiette: - unité: € - -Barème linéaire en taux: - formule: - barème linéaire: - assiette: assiette - tranches: - - de: 0 - à: 999 - taux: 5% - - de: 1000 - à: 1999 - taux: 10% - - au-dessus de: 2000 - taux: 15% - unité attendue: € - - exemples: - - nom: 'petite assiette' - situation: - assiette: 200 - valeur attendue: 10 - - nom: 'moyenne assiette' - situation: - assiette: 1500 - valeur attendue: 150 - - nom: 'grande assiette' - situation: - assiette: 10000 - valeur attendue: 1500 - - nom: "pour choisir la tranche, l'assiette est arrondie au préalable" - situation: - assiette: 999.3 - valeur attendue: 49.965 - - nom: "pour choisir la tranche, l'assiette est arrondie au préalable (2)" - situation: - assiette: 999.6 - valeur attendue: 99.96 - -Barème linéaire en montant: - formule: - barème linéaire: - assiette: assiette - tranches: - - de: 0 - à: 999 - montant: 50 - - de: 1000 - à: 1999 - montant: 170 - - au-dessus de: 2000 - montant: 400 - - unité attendue: € - exemples: - - nom: 'petite assiette' - situation: - assiette: 200 - valeur attendue: 50 - - nom: 'moyenne assiette' - situation: - assiette: 1500 - valeur attendue: 170 - - nom: 'grande assiette' - situation: - assiette: 10000 - valeur attendue: 400 - - nom: "pour choisir la tranche, l'assiette est arrondie au préalable" - situation: - assiette: 999.3 - valeur attendue: 50 - - nom: "pour choisir la tranche, l'assiette est arrondie au préalable (2)" - situation: - assiette: 999.6 - valeur attendue: 170 diff --git a/test/mécanismes/barème.yaml b/test/mécanismes/barème.yaml index 218edb6ce..e3f4d212d 100644 --- a/test/mécanismes/barème.yaml +++ b/test/mécanismes/barème.yaml @@ -1,8 +1,8 @@ assiette: - unité: € + unité: €/mois base: - unité: € + unité: €/mois Barème en taux marginaux: formule: @@ -10,14 +10,12 @@ Barème en taux marginaux: assiette: assiette multiplicateur: base tranches: - - en-dessous de: 1 - taux: 4.65% - - de: 1 - à: 3 - taux: 3% - - au-dessus de: 3 - taux: 1% - unité attendue: € + - taux: 4.65% + plafond: 1 + - taux: 3% + plafond: 3 + - taux: 1% + unité attendue: €/mois exemples: - nom: 'petite assiette' situation: @@ -42,16 +40,14 @@ Barème à composantes: multiplicateur: base composantes: - tranches: - - en-dessous de: 1 - taux: 2% - - au-dessus de: 1 - taux: 0% + - taux: 2% + plafond: 1 + - taux: 0% - tranches: - - en-dessous de: 2 - taux: 9% - - au-dessus de: 2 - taux: 29% - unité attendue: € + - taux: 9% + plafond: 2 + - taux: 29% + unité attendue: €/mois exemples: - nom: @@ -78,11 +74,10 @@ deuxième barème: assiette: assiette multiplicateur: base tranches: - - en-dessous de: 1 - taux: taux variable - - au-dessus de: 1 - taux: 90% - unité attendue: € + - taux: taux variable + plafond: 1 + - taux: 90% + unité attendue: '€/mois' exemples: - nom: taux faible @@ -109,3 +104,37 @@ deuxième barème: base: 100 variables manquantes: - ma condition + +tranche 1: + formule: 100 €/mois +tranche 2: + unité: €/an +tranches variables: + formule: + barème: + assiette: assiette + tranches: + - taux: 10% + plafond: tranche 1 + - taux: 50% + plafond: tranche 2 + exemples: + - nom: tranche 2 manquante non active + situation: + assiette: 40 + valeur attendue: 4 + - nom: tranche 2 manquante active + situation: + assiette: 200 + variables manquantes: + - tranche 2 + - nom: tranche 2 active + situation: + assiette: 200 + tranche 2: 12000 + valeur attendue: 60 # 10% * 100 + 50% * 100 + - nom: tranche 2 dépassée + situation: + assiette: 2000 + tranche 2: 12000 + valeur attendue: 460 # 10% * 100 + 50% * 900 diff --git a/test/mécanismes/complément.yaml b/test/mécanismes/complément.yaml deleted file mode 100644 index 613155a99..000000000 --- a/test/mécanismes/complément.yaml +++ /dev/null @@ -1,35 +0,0 @@ -ma cotisation: - unité: € - -Complément: - formule: - complément: - cible: ma cotisation - montant: 100 - - exemples: - - nom: - situation: - ma cotisation: 33 - valeur attendue: 67 - -autre cotisation: - unité: € - -Complément à composantes: - formule: - complément: - composantes: - - nom: A - cible: ma cotisation - montant: 100 - - nom: B - cible: autre cotisation - montant: 200 - - exemples: - - nom: - situation: - ma cotisation: 33 - autre cotisation: 133 - valeur attendue: 134 diff --git a/test/mécanismes/conversion-unité.yaml b/test/mécanismes/conversion-unité.yaml index 3dd202f03..a096e55bd 100644 --- a/test/mécanismes/conversion-unité.yaml +++ b/test/mécanismes/conversion-unité.yaml @@ -72,15 +72,13 @@ Conversion de mécanisme 1: unité: €/an formule: barème: - assiette: assiette mensuelle [€/an] + assiette: assiette mensuelle tranches: - - en-dessous de: 30000 - taux: 4.65% - - de: 30000 - à: 90000 - taux: 3% - - au-dessus de: 90000 - taux: 1% + - taux: 4.65% + plafond: 30000 €/an + - taux: 3% + plafond: 90000 €/an + - taux: 1% exemples: - situation: @@ -93,15 +91,13 @@ assiette annuelle: Conversion de mécanisme 2: formule: barème: - assiette: assiette annuelle [€/mois] + assiette: assiette annuelle tranches: - - en-dessous de: 2500 - taux: 4.65% - - de: 2500 - à: 7500 - taux: 3% - - au-dessus de: 7500 - taux: 1% + - taux: 4.65% + plafond: 2500 €/mois + - taux: 3% + plafond: 7500 €/mois + - taux: 1% exemples: - situation: assiette annuelle: 36000 @@ -126,8 +122,8 @@ retraite: formule: multiplication: assiette: assiette annuelle - plafond: 12 k€/an taux: 10% + plafond: 12 k€/an Conversion dans une somme compliquée: formule: diff --git a/test/mécanismes/expressions.yaml b/test/mécanismes/expressions.yaml index fe3dc1c66..5d704dbad 100644 --- a/test/mécanismes/expressions.yaml +++ b/test/mécanismes/expressions.yaml @@ -98,7 +98,7 @@ nombre de personnes: division trois: formule: salaire de base / nombre de personnes - unité attendue: $ / personne + unité attendue: $/personne exemples: - situation: salaire de base: 3000 diff --git a/test/mécanismes/grille.yaml b/test/mécanismes/grille.yaml new file mode 100644 index 000000000..30402c554 --- /dev/null +++ b/test/mécanismes/grille.yaml @@ -0,0 +1,33 @@ +assiette: + unité: € + +Grille: + formule: + grille: + assiette: assiette + unité: € + tranches: + - montant: 50 + plafond: 1000 € + - montant: 170 + plafond: 2000 € + - montant: 400 + + unité attendue: € + exemples: + - nom: 'petite assiette' + situation: + assiette: 200 + valeur attendue: 50 + - nom: 'moyenne assiette' + situation: + assiette: 1500 + valeur attendue: 170 + - nom: 'grande assiette' + situation: + assiette: 10000 + valeur attendue: 400 + - nom: 'assiette limite' + situation: + assiette: 999.3 + valeur attendue: 50 diff --git a/test/mécanismes/multiplication.yaml b/test/mécanismes/multiplication.yaml index 3194b0b4d..4e0270e18 100644 --- a/test/mécanismes/multiplication.yaml +++ b/test/mécanismes/multiplication.yaml @@ -70,8 +70,8 @@ Multiplication complète: multiplication: assiette: mon assiette facteur: mon facteur - plafond: mon plafond taux: 0.5% + plafond: mon plafond unité attendue: €.patates exemples: diff --git a/test/mécanismes/taux-progressif.yaml b/test/mécanismes/taux-progressif.yaml new file mode 100644 index 000000000..3ca412a0c --- /dev/null +++ b/test/mécanismes/taux-progressif.yaml @@ -0,0 +1,47 @@ +base: + unité: £ + formule: 300 + +assiette: + unité: £ + +base deux: + unité: µ + formule: 300 + +assiette deux: + unité: µ + +Simple: + unité: '%' + formule: + taux progressif: + assiette: assiette deux + multiplicateur: base deux + tranches: + - plafond: 0.75 + taux: 100% + - plafond: 1 + taux: 0% + unité attendue: '%' + exemples: + - nom: Premier point + situation: + assiette deux: 200 + valeur attendue: 100 + - nom: Deuxième point + situation: + assiette deux: 225 + valeur attendue: 100 + - nom: Troisième point + situation: + assiette deux: 262.5 + valeur attendue: 50 + - nom: Quatrième point + situation: + assiette deux: 300 + valeur attendue: 0 + - nom: Cinquième point + situation: + assiette deux: 300 + valeur attendue: 0 diff --git a/test/regressions/__snapshots__/simulations.jest.js.snap b/test/regressions/__snapshots__/simulations.jest.js.snap index eed0b5b0b..fed0cfbeb 100644 --- a/test/regressions/__snapshots__/simulations.jest.js.snap +++ b/test/regressions/__snapshots__/simulations.jest.js.snap @@ -44,7 +44,7 @@ exports[`calculate simulations-auto-entrepreneur: échelle de revenus 9`] = `"[ exports[`calculate simulations-auto-entrepreneur: échelle de revenus 10`] = `"[1148303,148303,1000000,131979,868021]"`; -exports[`calculate simulations-indépendant: acre 1`] = `"[73028,23028,50000,51980,8052,41948,null,73028]"`; +exports[`calculate simulations-indépendant: acre 1`] = `"[73024,23024,50000,51980,8052,41948,null,73024]"`; exports[`calculate simulations-indépendant: activité 1`] = `"[28923,8923,20000,20783,604,19396,null,28923]"`; @@ -52,7 +52,7 @@ exports[`calculate simulations-indépendant: activité 2`] = `"[29101,9101,2000 exports[`calculate simulations-indépendant: impôt sur le revenu 1`] = `"[29085,9085,20000,20787,603,19397,null,29085]"`; -exports[`calculate simulations-indépendant: impôt sur le revenu 2`] = `"[73028,23028,50000,51980,8213,41787,null,73028]"`; +exports[`calculate simulations-indépendant: impôt sur le revenu 2`] = `"[73024,23024,50000,51980,8213,41787,null,73024]"`; exports[`calculate simulations-indépendant: impôt sur le revenu 3`] = `"[29085,9085,20000,20787,2079,17921,null,29085]"`; @@ -62,7 +62,7 @@ exports[`calculate simulations-indépendant: inversions 2`] = `"[50000,16003,33 exports[`calculate simulations-indépendant: inversions 3`] = `"[14596,4596,10000,10394,0,10000,null,14596]"`; -exports[`calculate simulations-indépendant: inversions 4`] = `"[88551,27364,61187,63588,11187,50000,null,88551]"`; +exports[`calculate simulations-indépendant: inversions 4`] = `"[88547,27360,61187,63588,11187,50000,null,88547]"`; exports[`calculate simulations-indépendant: inversions 5`] = `"[14596,4596,10000,10394,0,10000,null,15596]"`; @@ -82,9 +82,9 @@ exports[`calculate simulations-indépendant: échelle de revenus 5`] = `"[7427, exports[`calculate simulations-indépendant: échelle de revenus 6`] = `"[14596,4596,10000,10394,0,10000,null,14596]"`; -exports[`calculate simulations-indépendant: échelle de revenus 7`] = `"[139598,39598,100000,103788,24245,75755,null,139598]"`; +exports[`calculate simulations-indépendant: échelle de revenus 7`] = `"[139594,39594,100000,103788,24245,75755,null,139594]"`; -exports[`calculate simulations-indépendant: échelle de revenus 8`] = `"[1239743,239743,1000000,1033661,467503,532497,null,1239743]"`; +exports[`calculate simulations-indépendant: échelle de revenus 8`] = `"[1239955,239955,1000000,1033666,467505,532495,null,1239955]"`; exports[`calculate simulations-rémunération-dirigeant: Assimilé salarié - ACRE 1`] = `"[7257,7257,7184,4,13,16]"`; @@ -186,7 +186,7 @@ exports[`calculate simulations-rémunération-dirigeant: Indépendant - échell exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 6`] = `"[30434,33997,24912,4,48,0]"`; -exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 7`] = `"[56271,69892,36442,4,56,0]"`; +exports[`calculate simulations-rémunération-dirigeant: Indépendant - échelle de rémunération 7`] = `"[56273,69895,36431,4,56,0]"`; exports[`calculate simulations-salarié: JEI 1`] = `"[3440,0,0,3000,2353,2187]"`; diff --git a/test/rules/sasu.yaml b/test/rules/sasu.yaml index f231cf4ca..4a31a6522 100644 --- a/test/rules/sasu.yaml +++ b/test/rules/sasu.yaml @@ -15,15 +15,13 @@ répartition salaire sur dividendes: impôt sur les sociétés: formule: barème: - assiette: bénéfice [€/an] + assiette: bénéfice tranches: - - en-dessous de: 38120 - taux: 15% - - de: 38120 - à: 500000 - taux: 28% - - au-dessus de: 500000 - taux: 33.3% + - taux: 15% + plafond: 38120 €/an + - taux: 28% + plafond: 500000 €/an + - taux: 33.3% références: fiche service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23575 diff --git a/test/traverse.test.js b/test/traverse.test.js index cd6206ad9..4bd223b1a 100644 --- a/test/traverse.test.js +++ b/test/traverse.test.js @@ -196,9 +196,9 @@ describe('analyse with mecanisms', function() { assiette: 2008, multiplicateur: 1000, tranches: [ - { 'en-dessous de': 1, taux: 0.1 }, - { de: 1, à: 2, taux: 1.2 }, - { 'au-dessus de': 2, taux: 10 } + { plafond: 1, taux: 0.1 }, + { plafond: 2, taux: 1.2 }, + { taux: 10 } ] } } @@ -221,16 +221,16 @@ describe('analyse with mecanisms', function() { composantes: [ { tranches: [ - { 'en-dessous de': 1, taux: 0.05 }, - { de: 1, à: 2, taux: 0.4 }, - { 'au-dessus de': 2, taux: 5 } + { plafond: 1, taux: 0.05 }, + { plafond: 2, taux: 0.4 }, + { taux: 5 } ] }, { tranches: [ - { 'en-dessous de': 1, taux: 0.05 }, - { de: 1, à: 2, taux: 0.8 }, - { 'au-dessus de': 2, taux: 5 } + { plafond: 1, taux: 0.05 }, + { plafond: 2, taux: 0.8 }, + { taux: 5 } ] } ] @@ -257,9 +257,9 @@ describe('analyse with mecanisms', function() { si: '3 > 4', alors: { tranches: [ - { 'en-dessous de': 1, taux: 0.1 }, - { de: 1, à: 2, taux: 1.2 }, - { 'au-dessus de': 2, taux: 10 } + { plafond: 1, taux: 0.1 }, + { plafond: 2, taux: 1.2 }, + { taux: 10 } ] } }, @@ -267,9 +267,9 @@ describe('analyse with mecanisms', function() { si: '3 > 2', alors: { tranches: [ - { 'en-dessous de': 1, taux: 0.1 }, - { de: 1, à: 2, taux: 1.8 }, - { 'au-dessus de': 2, taux: 10 } + { plafond: 1, taux: 0.1 }, + { plafond: 2, taux: 1.8 }, + { taux: 10 } ] } } @@ -294,41 +294,6 @@ describe('analyse with mecanisms', function() { ).to.have.property('nodeValue', 3200) }) - it('should handle complements', function() { - let rawRules = [ - { nom: 'top' }, - { - nom: 'top . startHere', - formule: { complément: { cible: 'dix', montant: 93 } } - }, - { nom: 'top . dix', formule: 17 } - ], - rules = parseAll(rawRules.map(enrichRule)) - expect( - analyse(rules, 'startHere')(stateSelector).targets[0] - ).to.have.property('nodeValue', 93 - 17) - }) - - it('should handle components in complements', function() { - let rawRules = [ - { nom: 'top' }, - { - nom: 'top . startHere', - formule: { - complément: { - cible: 'dix', - composantes: [{ montant: 93 }, { montant: 93 }] - } - } - }, - { nom: 'top . dix', formule: 17 } - ], - rules = parseAll(rawRules.map(enrichRule)) - expect( - analyse(rules, 'startHere')(stateSelector).targets[0] - ).to.have.property('nodeValue', 2 * (93 - 17)) - }) - it('should handle filtering on components', function() { let rawRules = [ { nom: 'top' }, @@ -342,17 +307,17 @@ describe('analyse with mecanisms', function() { composantes: [ { tranches: [ - { 'en-dessous de': 1, taux: 0.05 }, - { de: 1, à: 2, taux: 0.4 }, - { 'au-dessus de': 2, taux: 5 } + { plafond: 1, taux: 0.05 }, + { plafond: 2, taux: 0.4 }, + { 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 } + { plafond: 1, taux: 0.05 }, + { plafond: 2, taux: 0.8 }, + { taux: 5 } ], attributs: { 'dû par': 'employeur' } } @@ -384,17 +349,17 @@ describe('analyse with mecanisms', function() { composantes: [ { tranches: [ - { 'en-dessous de': 1, taux: 0.05 }, - { de: 1, à: 2, taux: 0.4 }, - { 'au-dessus de': 2, taux: 5 } + { plafond: 1, taux: 0.05 }, + { plafond: 2, taux: 0.4 }, + { 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 } + { plafond: 1, taux: 0.05 }, + { plafond: 2, taux: 0.8 }, + { taux: 5 } ], attributs: { 'dû par': 'employeur' } }