diff --git a/modele-social/README.md b/modele-social/README.md index 2a6cadbce..02e875d4b 100644 --- a/modele-social/README.md +++ b/modele-social/README.md @@ -2,27 +2,28 @@ Ce paquet contient les règles publicodes utilisées sur https://mon-entreprise.fr pour le calcul des cotisations sociales, des impôts et des droits sociaux. + ### Installation + ``` -npm install publicodes modele-social +npm install publicodes modele-social ``` ### Exemple d'utilisation -```js -import Engine, { formatValue } from "publicodes"; -import rules from "modele-social"; -const engine = new Engine(rules); +```js +import Engine, { formatValue } from 'publicodes' +import rules from 'modele-social' + +const engine = new Engine(rules) const net = engine - .setSituation({ - "contrat salarié . rémunération . brut de base": "3000 €/mois", - }) - .evaluate("contrat salarié . rémunération . net"); - -console.log(formatValue(net)); + .setSituation({ + 'contrat salarié . rémunération . brut de base': '3000 €/mois', + }) + .evaluate('contrat salarié . rémunération . net') +console.log(formatValue(net)) ``` - 👉 **[Voir le tutoriel complet](https://mon-entreprise.fr/int%C3%A9gration/biblioth%C3%A8que-de-calcul)** diff --git a/mon-entreprise/source/App.tsx b/mon-entreprise/source/App.tsx index 79080655d..b66e9a37c 100644 --- a/mon-entreprise/source/App.tsx +++ b/mon-entreprise/source/App.tsx @@ -4,14 +4,13 @@ import Header from 'Components/layout/Header' import Route404 from 'Components/Route404' import 'Components/ui/index.css' import { - engineOptions, + engineFactory, EngineProvider, + Rules, SituationProvider, } from 'Components/utils/EngineContext' import { SitePathsContext } from 'Components/utils/SitePathsContext' import 'iframe-resizer' -import { DottedName } from 'modele-social' -import Engine, { Rule } from 'publicodes' import { useContext, useMemo } from 'react' import { Helmet } from 'react-helmet' import { useTranslation } from 'react-i18next' @@ -70,16 +69,13 @@ const middlewares = [createSentryMiddleware(Sentry as any)] type RootProps = { basename: ProviderProps['basename'] - rules: Record + rules: Rules } export default function Root({ basename, rules }: RootProps) { const { language } = useTranslation().i18n const paths = constructLocalizedSitePath(language as 'fr' | 'en') - const engine = useMemo(() => new Engine(rules, engineOptions), [ - rules, - engineOptions, - ]) + const engine = useMemo(() => engineFactory(rules), [rules]) return ( - new Engine(parsedRules, engineOptions).setSituation({ + engine.shallowCopy().setSituation({ ...situation, dirigeant: "'assimilé salarié'", }), @@ -59,7 +56,7 @@ export default function SchemeComparaison({ ) const autoEntrepreneurEngine = useMemo( () => - new Engine(parsedRules, engineOptions).setSituation({ + engine.shallowCopy().setSituation({ ...situation, dirigeant: "'auto-entrepreneur'", }), @@ -67,7 +64,7 @@ export default function SchemeComparaison({ ) const indépendantEngine = useMemo( () => - new Engine(parsedRules, engineOptions).setSituation({ + engine.shallowCopy().setSituation({ ...situation, dirigeant: "'indépendant'", }), diff --git a/mon-entreprise/source/components/utils/EngineContext.tsx b/mon-entreprise/source/components/utils/EngineContext.tsx index 16318ce92..7891de9cb 100644 --- a/mon-entreprise/source/components/utils/EngineContext.tsx +++ b/mon-entreprise/source/components/utils/EngineContext.tsx @@ -1,14 +1,12 @@ -import Engine from 'publicodes' +import Engine, { Rule } from 'publicodes' import React, { createContext, useContext } from 'react' import { DottedName } from 'modele-social' import i18n from '../../locales/i18n' -export const EngineContext = createContext(new Engine({})) -export const EngineProvider = EngineContext.Provider +export type Rules = Record const unitsTranslations = Object.entries(i18n.getResourceBundle('fr', 'units')) - -export const engineOptions = { +const engineOptions = { getUnitKey(unit: string): string { const key = unitsTranslations .find(([, trans]) => trans === unit)?.[0] @@ -19,6 +17,12 @@ export const engineOptions = { return i18n?.t(`units:${unit}`, { count }) }, } +export function engineFactory(rules: Rules) { + return new Engine(rules, engineOptions) +} + +export const EngineContext = createContext(new Engine()) +export const EngineProvider = EngineContext.Provider export function useEngine(): Engine { return useContext(EngineContext) as Engine diff --git a/mon-entreprise/source/pages/Simulateurs/AidesEmbauche.tsx b/mon-entreprise/source/pages/Simulateurs/AidesEmbauche.tsx index 193b0fa74..e03cc1c1e 100644 --- a/mon-entreprise/source/pages/Simulateurs/AidesEmbauche.tsx +++ b/mon-entreprise/source/pages/Simulateurs/AidesEmbauche.tsx @@ -10,7 +10,7 @@ import { useSimulationProgress } from 'Components/utils/useNextQuestion' import { useParamsFromSituation } from 'Components/utils/useSearchParamsSimulationSharing' import useSimulationConfig from 'Components/utils/useSimulationConfig' import { DottedName } from 'modele-social' -import Engine, { formatValue } from 'publicodes' +import { formatValue } from 'publicodes' import { partition } from 'ramda' import { useContext } from 'react' import { Trans, useTranslation } from 'react-i18next' @@ -229,7 +229,7 @@ function Results() { const progress = useSimulationProgress() const baseEngine = useEngine() const aidesEngines = aides.map((aide) => { - const engine = new Engine(baseEngine.parsedRules) + const engine = baseEngine.shallowCopy() engine.setSituation({ ...aide.situation, ...baseEngine.parsedSituation }) const isActive = typeof engine.evaluate(aide.dottedName).nodeValue === 'number' diff --git a/mon-entreprise/test/real-rules.test.js b/mon-entreprise/test/real-rules.test.js index dad097818..09ca149e1 100644 --- a/mon-entreprise/test/real-rules.test.js +++ b/mon-entreprise/test/real-rules.test.js @@ -6,7 +6,7 @@ import rules from 'modele-social' // les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle, // comme dans sa formule let parsedRules = parsePublicodes(rules) -const engine = new Engine(parsedRules) +const engine = new Engine(rules) let runExamples = (examples, rule) => examples.map((ex) => { const expected = ex['valeur attendue'] diff --git a/mon-entreprise/test/regressions/simulations.jest.js b/mon-entreprise/test/regressions/simulations.jest.js index 4b1777478..4c020a6d9 100644 --- a/mon-entreprise/test/regressions/simulations.jest.js +++ b/mon-entreprise/test/regressions/simulations.jest.js @@ -6,9 +6,8 @@ // renamed the test configuration may be adapted but the persisted snapshot will remain unchanged). /* eslint-disable no-undef */ -import Engine from 'publicodes' import rules from '../../../modele-social' -import { engineOptions } from '../../source/components/utils/EngineContext' +import { engineFactory } from '../../source/components/utils/EngineContext' import aideDéclarationConfig from '../../source/pages/Gérer/AideDéclarationIndépendant/config.yaml' import artisteAuteurConfig from '../../source/pages/Simulateurs/configs/artiste-auteur.yaml' import autoentrepreneurConfig from '../../source/pages/Simulateurs/configs/auto-entrepreneur.yaml' @@ -26,7 +25,7 @@ import remunerationDirigeantSituations from './simulations-rémunération-dirige import employeeSituations from './simulations-salarié.yaml' const roundResult = (arr) => arr.map((x) => Math.round(x)) -const engine = new Engine(rules, engineOptions) +const engine = engineFactory(rules) const runSimulations = (situations, targets, baseSituation = {}) => Object.entries(situations).map(([name, situations]) => situations.forEach((situation) => { diff --git a/mon-entreprise/test/useSearchParamsSimulationSharing.test.js b/mon-entreprise/test/useSearchParamsSimulationSharing.test.js index 02ee5e9bf..d24d0a3e9 100644 --- a/mon-entreprise/test/useSearchParamsSimulationSharing.test.js +++ b/mon-entreprise/test/useSearchParamsSimulationSharing.test.js @@ -26,14 +26,13 @@ describe('identifiant court', () => { }) describe('useSearchParamsSimulationSharing', () => { - const someRules = parsePublicodes(` + const engine = new Engine(` rule with: identifiant court: panta formule: 0 rule without: formule: 0 `) - const engine = new Engine(someRules) const dottedNameParamName = getRulesParamNames(engine.getParsedRules()) describe('getSearchParamsFromSituation', () => { @@ -92,7 +91,7 @@ rule without: }) describe('useSearchParamsSimulationSharing hook', () => { - const someRules = parsePublicodes(` + const parsedRules = parsePublicodes(` rule with: identifiant court: panta formule: 0 @@ -100,9 +99,7 @@ rule without: formule: 0 `) - const dottedNameParamName = getRulesParamNames( - new Engine(someRules).getParsedRules() - ) + const dottedNameParamName = getRulesParamNames(parsedRules) let setSearchParams beforeEach(() => { diff --git a/publicodes/core/source/index.ts b/publicodes/core/source/index.ts index 13d540abc..e61b1b199 100644 --- a/publicodes/core/source/index.ts +++ b/publicodes/core/source/index.ts @@ -67,6 +67,7 @@ export type ParsedRules = Record< Name, RuleNode & { dottedName: Name } > + export default class Engine { parsedRules: ParsedRules parsedSituation: Record = {} @@ -75,25 +76,11 @@ export default class Engine { options: Options constructor( - rules: string | Record | ParsedRules = {}, + rules: string | Record = {}, options: Partial = {} ) { this.options = { ...options, logger: options.logger ?? console } - if (typeof rules === 'string') { - rules = parsePublicodes(rules, this.options) as ParsedRules - } - const firstRuleObject = Object.values(rules)[0] as Rule | RuleNode - if ( - typeof firstRuleObject !== 'object' || - firstRuleObject == null || - !('nodeKind' in firstRuleObject) - ) { - rules = parsePublicodes( - rules as Record, - this.options - ) as ParsedRules - } - this.parsedRules = rules as ParsedRules + this.parsedRules = parsePublicodes(rules, this.options) as ParsedRules this.replacements = getReplacements(this.parsedRules) } @@ -150,6 +137,10 @@ export default class Engine { return this.parsedRules } + getOptions(): Options { + return this.options + } + evaluate(value: N): N & EvaluatedNode evaluate(value: PublicodesExpression): EvaluatedNode evaluate(value: PublicodesExpression | ASTNode): EvaluatedNode { @@ -180,6 +171,17 @@ export default class Engine { this.cache.nodes.set(value, evaluatedNode) return evaluatedNode } + + /** + * Shallow Engine instance copy. Keeps references to the original Engine instance attributes. + */ + shallowCopy(): Engine { + const newEngine = new Engine() + newEngine.options = this.options + newEngine.parsedRules = this.parsedRules + newEngine.replacements = this.replacements + return newEngine + } } /** diff --git a/publicodes/core/test/inversion.test.js b/publicodes/core/test/inversion.test.js index f0c2089d9..2de306c83 100644 --- a/publicodes/core/test/inversion.test.js +++ b/publicodes/core/test/inversion.test.js @@ -51,7 +51,7 @@ describe('inversions', () => { assiette: brut taux: 77% - brut: + brut: formule: inversion numérique: unité: € @@ -147,7 +147,7 @@ describe('inversions', () => { formule: produit: assiette: assiette - taux: + taux: variations: - si: cadre alors: 80% diff --git a/publicodes/ui-react/source/mecanisms/common.tsx b/publicodes/ui-react/source/mecanisms/common.tsx index d96967084..6e4dd8c06 100644 --- a/publicodes/ui-react/source/mecanisms/common.tsx +++ b/publicodes/ui-react/source/mecanisms/common.tsx @@ -64,7 +64,7 @@ export const NodeValuePointer = ({ data, unit }: NodeValuePointerProps) => { }} > {formatValue(simplifyNodeUnit({ nodeValue: data, unit }), { - formatUnit: engine?.options?.formatUnit, + formatUnit: engine?.getOptions()?.formatUnit, })} )