From 8cc5615e0f4872118148ec1b11d2316d0255437e Mon Sep 17 00:00:00 2001 From: Johan Girod Date: Wed, 9 Mar 2022 16:52:09 +0100 Subject: [PATCH] wip --- "modele-social/r\303\250gles/dirigeant.yaml" | 34 ++--- .../r\303\250gles/entreprise/imposition.yaml" | 12 +- .../entreprise/statut-juridique.yaml" | 55 ++++--- site/source/components/EngineValue.tsx | 9 +- .../components/conversation/Conversation.tsx | 8 +- .../components/utils/useSimulationConfig.ts | 31 +++- site/source/hooks/useObjectifsDependencies.ts | 36 +++++ site/source/pages/Gerer/Home.tsx | 142 +++++++----------- site/source/reducers/inFranceAppReducer.ts | 3 + site/source/selectors/simulationSelectors.ts | 4 +- 10 files changed, 194 insertions(+), 140 deletions(-) create mode 100644 site/source/hooks/useObjectifsDependencies.ts diff --git "a/modele-social/r\303\250gles/dirigeant.yaml" "b/modele-social/r\303\250gles/dirigeant.yaml" index d7f85dce86..b91ae7a87f 100644 --- "a/modele-social/r\303\250gles/dirigeant.yaml" +++ "b/modele-social/r\303\250gles/dirigeant.yaml" @@ -2,32 +2,33 @@ dirigeant: dirigeant . minoritaire: titre: Gérant minoritaire ou égalitaire non applicable si: - - entreprise . catégories juridiques . EI - - entreprise . catégories juridiques . SARL . unipersonnelle - - entreprise . catégories juridiques . SAS . unipersonnelle + une de ces conditions: + - entreprise . catégorie juridique . EI + - entreprise . catégorie juridique . SARL . unipersonnelle + - entreprise . catégorie juridique . SAS . unipersonnelle par défaut: non dirigeant . régime social: par défaut: non variations: - - si: entreprise . auto-entrepreneur + - si: entreprise . catégorie juridique . EI . auto-entrepreneur alors: "'auto-entrepreneur'" - si: une de ces conditions: - - entreprise . catégories juridiques . EI - - entreprise . catégories juridiques . SARL - - entreprise . catégories juridiques . SELARL + - entreprise . catégorie juridique . EI + - entreprise . catégorie juridique . SARL + - entreprise . catégorie juridique . SELARL non applicable si: dirigeant . minoritaire - alors: "'indépendant'" + alors: "'indépendant'" - si: une de ces conditions: - - entreprise . catégories juridiques . SAS - - entreprise . catégories juridiques . SELAS + - entreprise . catégorie juridique . SAS + - entreprise . catégorie juridique . SELAS - toutes ces conditions: - dirigeant . minoritaire - une de ces conditions: - - entreprise . catégories juridiques . SARL - - entreprise . catégories juridiques . SELARL + - entreprise . catégorie juridique . SARL + - entreprise . catégorie juridique . SELARL alors: "'assimilé salarié'" dirigeant . rémunération: oui @@ -103,13 +104,12 @@ dirigeant . assimilé salarié: description: | Certains dirigeants d'entreprise (c'est notamment le cas pour les SASU) sont considérés par la sécurité sociale comme assimilés aux salariés. Ils sont alors au régime général de la sécurité sociale, avec quelques contraintes cependant. Par exemple, ils ne cotisent pas au chômage, et n'y ont donc pas droit. applicable si: régime social = 'assimilé salarié' + valeur: oui remplace: - règle: contrat salarié par: "'CDI'" - règle: contrat salarié . statut cadre par: oui - - règle: entreprise . imposition - par: "'IS'" rend non applicable: - contrat salarié . convention collective - contrat salarié . activité partielle @@ -175,11 +175,6 @@ dirigeant . assimilé salarié . réduction ACRE . notification taux annuel: dirigeant . auto-entrepreneur: applicable si: régime social = 'auto-entrepreneur' rend non applicable: contrat salarié - remplace: - - règle: entreprise . imposition - par: "'IR'" - - règle: entreprise . imposition . IR . micro-fiscal - par: oui icônes: 🚶 description: | L'auto-entreprise est une entreprise individuelle simplifiée. À l'origine connu sous l'appellation « auto-entrepreneur », le régime de « micro-entrepreneur » est un régime de travailleur indépendant créé pour simplifier la gestion administrative, notamment en remplaçant toutes les cotisations sociales par un prélèvement unique mensuel. @@ -517,6 +512,7 @@ dirigeant . auto-entrepreneur . chiffre d'affaires: dirigeant . indépendant: applicable si: régime social = 'indépendant' + valeur: oui rend non applicable: contrat salarié dirigeant . indépendant . revenu professionnel: diff --git "a/modele-social/r\303\250gles/entreprise/imposition.yaml" "b/modele-social/r\303\250gles/entreprise/imposition.yaml" index 3d08bdd8ed..cb716d14d8 100644 --- "a/modele-social/r\303\250gles/entreprise/imposition.yaml" +++ "b/modele-social/r\303\250gles/entreprise/imposition.yaml" @@ -9,11 +9,16 @@ entreprise . imposition: possibilités: - IR - IS - par défaut: "'IR'" + par défaut: + variations: + - si: catégorie juridique . EI + alors: "'IR'" + - sinon: "'IS'" entreprise . imposition . IR: - valeur: imposition = 'IR' + applicable si: imposition = 'IR' titre: Impôt sur le revenu + valeur: oui entreprise . imposition . IR . micro-fiscal: rend non applicable: dirigeant . indépendant . cotisations facultatives @@ -112,7 +117,8 @@ entreprise . imposition . IR . information sur le report de déficit: bofip: https://bofip.impots.gouv.fr/bofip/2003-PGP.html/identifiant%3DBOI-BIC-DEF-20-10-20170301 entreprise . imposition . IS: - valeur: imposition = 'IS' + applicable si: imposition = 'IS' + valeur: oui titre: Impôt sur les sociétés entreprise . imposition . IS . résultat imposable: diff --git "a/modele-social/r\303\250gles/entreprise/statut-juridique.yaml" "b/modele-social/r\303\250gles/entreprise/statut-juridique.yaml" index 37bd7ed5bd..48f7576481 100644 --- "a/modele-social/r\303\250gles/entreprise/statut-juridique.yaml" +++ "b/modele-social/r\303\250gles/entreprise/statut-juridique.yaml" @@ -1,12 +1,4 @@ -entreprise . auto-entrepreneur: - par défaut: non - question: Êtes-vous auto-entrepreneur ? - remplace: - règle: entreprise . catégories juridiques - par: "'EI'" - applicable si: catégories juridiques = 'EI' - -entreprise . catégories juridiques: +entreprise . catégorie juridique: description: | Les catégories juridiques accessibles via l'API SIRENE une possibilité: @@ -20,42 +12,63 @@ entreprise . catégories juridiques: liste des catégories juridique de l'INSEE: https://www.insee.fr/fr/information/2028129 note: On se base ici sur les catégories juridiques définies par l'INSEE -entreprise . catégories juridiques . EI: +entreprise . catégorie juridique . EI: titre: 'EI ou EIRL' - valeur: catégories juridiques = 'EI' + valeur: catégorie juridique = 'EI' + +entreprise . catégorie juridique . EI . auto-entrepreneur: + question: Êtes-vous auto-entrepreneur ? + remplace: + règle: imposition . IR . micro-fiscal + par: oui + par défaut: oui + +entreprise . catégorie juridique . EI . responsabilité limité: + non applicable si: auto-entrepreneur # pour simplifier + titre: 'EIRL' + question: Votre entreprise est-elle une EIRL ? + par défaut: non + +entreprise . catégorie juridique . EI . imposition entreprise: + non applicable si: responsabilité limité + remplace: entreprise . imposition + valeur: "'IR'" -entreprise . catégories juridiques . SARL: +entreprise . catégorie juridique . SARL: titre: 'EURL ou SARL' - valeur: catégories juridiques = 'SARL' + valeur: catégorie juridique = 'SARL' -entreprise . catégories juridiques . SARL . unipersonnelle: +entreprise . catégorie juridique . SARL . unipersonnelle: titre: EURL question: Votre entreprise est-elle une EURL ? par défaut: oui -entreprise . catégories juridiques . SELARL: +entreprise . catégorie juridique . SELARL: titre: 'SELARL' - valeur: catégories juridiques = 'SELARL' + valeur: catégorie juridique = 'SELARL' remplace: - règle: entreprise . activité par: "'libérale'" - règle: entreprise . activité . libérale réglementée par: oui -entreprise . catégories juridiques . SELAS: +entreprise . catégorie juridique . SELAS: titre: 'SELARL' - valeur: catégories juridiques = 'SELAS' + valeur: catégorie juridique = 'SELAS' remplace: - règle: entreprise . activité par: "'libérale'" - règle: entreprise . activité . libérale réglementée par: oui -entreprise . catégories juridiques . SAS: +entreprise . catégorie juridique . SAS: titre: 'SASU ou SAS' - valeur: catégories juridiques = 'SAS' + valeur: catégorie juridique = 'SAS' -entreprise . catégories juridiques . SAS . unipersonnelle: +entreprise . catégorie juridique . SAS . unipersonnelle: titre: 'SASU' question: Votre entreprise est-elle une SASU ? par défaut: oui + +entreprise . catégorie juridique . autre: + valeur: catégorie juridique = 'autre' diff --git a/site/source/components/EngineValue.tsx b/site/source/components/EngineValue.tsx index 3383428e7c..d87cb3142d 100644 --- a/site/source/components/EngineValue.tsx +++ b/site/source/components/EngineValue.tsx @@ -5,7 +5,6 @@ import Engine, { isNotApplicable, isNotYetDefined, PublicodesExpression, - UNSAFE_isNotApplicable, } from 'publicodes' import React from 'react' import { useTranslation } from 'react-i18next' @@ -94,7 +93,9 @@ export function WhenApplicable({ children: React.ReactNode }) { const engine = useEngine() - if (UNSAFE_isNotApplicable(engine, dottedName)) return null + if (engine.evaluate(dottedName).nodeValue == null) { + return null + } return <>{children} } export function WhenNotApplicable({ @@ -105,7 +106,9 @@ export function WhenNotApplicable({ children: React.ReactNode }) { const engine = useEngine() - if (!UNSAFE_isNotApplicable(engine, dottedName)) return null + if (engine.evaluate(dottedName).nodeValue !== null) { + return null + } return <>{children} } diff --git a/site/source/components/conversation/Conversation.tsx b/site/source/components/conversation/Conversation.tsx index fc241ac557..0c9b949e7d 100644 --- a/site/source/components/conversation/Conversation.tsx +++ b/site/source/components/conversation/Conversation.tsx @@ -24,10 +24,14 @@ import { ExplicableRule } from './Explicable' import SeeAnswersButton from './SeeAnswersButton' export type ConversationProps = { + displayNotification: boolean customEndMessages?: React.ReactNode } -export default function Conversation({ customEndMessages }: ConversationProps) { +export default function Conversation({ + customEndMessages, + displayNotification = true, +}: ConversationProps) { const dispatch = useDispatch() const engine = useContext(EngineContext) const currentQuestion = useNextQuestions()[0] @@ -107,7 +111,7 @@ export default function Conversation({ customEndMessages }: ConversationProps) { - + {displayNotification && } diff --git a/site/source/components/utils/useSimulationConfig.ts b/site/source/components/utils/useSimulationConfig.ts index 2bc4ddf658..92fe426f51 100644 --- a/site/source/components/utils/useSimulationConfig.ts +++ b/site/source/components/utils/useSimulationConfig.ts @@ -44,7 +44,7 @@ export function getCompanySituation(company: Company | null): Situation { ), }), ...(company?.categorieJuridiqueUniteLegale && { - 'entreprise . statut juridique': `'${getCatégorieFromCode( + 'entreprise . catégorie juridique': `'${getCatégorieFromCode( company.categorieJuridiqueUniteLegale )}'`, }), @@ -85,3 +85,32 @@ const getCatégorieFromCode = (code: string): CatégorieJuridique => { } return 'AUTRE' } + +// // Profession Libérale +// const inferPLSimulateurFromCompanyDetails = ( +// company: Company | null +// ): DirigeantOrNull => { +// if (!company) { +// return null +// } +// const activiteToSimulator = { +// 'Activités comptables': 'expert-comptable', +// 'Activité des médecins généralistes': 'médecin', +// 'Activités de radiodiagnostic et de radiothérapie': 'médecin', +// 'Activités chirurgicales': 'médecin', +// 'Activité des médecins spécialistes': 'médecin', +// 'Activités hospitalières': 'pamc', +// 'Pratique dentaire': 'chirurgien-dentiste', +// 'Commerce de détail de produits pharmaceutiques en magasin spécialisé': +// 'pharmacien', +// 'Activités des infirmiers et des sages-femmes': 'pamc', +// "Activités des professionnels de la rééducation, de l'appareillage et des pédicures-podologues": +// 'auxiliaire-médical', +// "Laboratoires d'analyses médicales": 'pharmacien', +// 'Arts du spectacle vivant': 'artiste-auteur', +// 'Création artistique relevant des arts plastiques': 'artiste-auteur', +// 'Autre création artistique': 'artiste-auteur', +// 'Activités photographiques': 'artiste-auteur', +// } as Record +// return activiteToSimulator[company.activitePrincipale] || null +// } diff --git a/site/source/hooks/useObjectifsDependencies.ts b/site/source/hooks/useObjectifsDependencies.ts new file mode 100644 index 0000000000..6065b50d30 --- /dev/null +++ b/site/source/hooks/useObjectifsDependencies.ts @@ -0,0 +1,36 @@ +import { useEngine } from '@/components/utils/EngineContext' +import { getNextSteps } from '@/components/utils/useNextQuestion' +import { + configSelector, + objectifsSelector, +} from '@/selectors/simulationSelectors' +import { DottedName } from 'modele-social' +import { useMemo } from 'react' +import { useSelector } from 'react-redux' + +function useObjectifs(): Array { + const objectifs = useSelector(objectifsSelector) + const config = useSelector(configSelector) + return useMemo(() => objectifs, [config]) +} +export function useObjectifsDeepDependencies(): Array { + const objectifs = useObjectifs() + const engine = useEngine() + const allMissings = useMemo(() => { + const dependencies = getNextSteps( + objectifs.map( + (obj) => ( + console.log(engine.evaluate(obj).missingVariables), + engine.evaluate(obj).missingVariables + ) + ) + ) + + return [...dependencies].filter( + (dottedName) => engine.getRule(dottedName).rawNode.question + ) + }, [objectifs]) + return allMissings.filter( + (dottedName) => engine.evaluate(dottedName).nodeValue !== null + ) +} diff --git a/site/source/pages/Gerer/Home.tsx b/site/source/pages/Gerer/Home.tsx index 69485ef39a..7138652fd2 100644 --- a/site/source/pages/Gerer/Home.tsx +++ b/site/source/pages/Gerer/Home.tsx @@ -1,23 +1,23 @@ import { DottedName } from '@/../../modele-social' -import Engine from '@/../../modele-social/node_modules/publicodes/dist' -import { specifyIfAutoEntrepreneur } from '@/actions/existingCompanyActions' -import CompanyDetails from '@/components/company/Details' +import Engine, { + PublicodesExpression, +} from '@/../../modele-social/node_modules/publicodes/dist' +import { updateSituation } from '@/actions/actions' +import RuleInput from '@/components/conversation/RuleInput' import { WhenApplicable, WhenNotApplicable } from '@/components/EngineValue' import PageHeader from '@/components/PageHeader' import { FromBottom } from '@/components/ui/animate' -import { EngineContext } from '@/components/utils/EngineContext' +import { useEngine } from '@/components/utils/EngineContext' import { ScrollToTop } from '@/components/utils/Scroll' import { SitePathsContext } from '@/components/utils/SitePathsContext' -import { Button } from '@/design-system/buttons' +import useSimulationConfig from '@/components/utils/useSimulationConfig' import { Container, Spacing } from '@/design-system/layout' -import Popover from '@/design-system/Popover' -import { H2 } from '@/design-system/typography/heading' -import { Link } from '@/design-system/typography/link' +import { H2, H3 } from '@/design-system/typography/heading' import { Intro } from '@/design-system/typography/paragraphs' -import { Company } from '@/reducers/inFranceAppReducer' +import { useObjectifsDeepDependencies } from '@/hooks/useObjectifsDependencies' import { RootState } from '@/reducers/rootReducer' import { Grid } from '@mui/material' -import { useContext, useEffect, useRef, useState } from 'react' +import { useContext } from 'react' import { Helmet } from 'react-helmet-async' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -38,12 +38,12 @@ import growth from './growth.svg' const infereSimulateurRevenuFromSituation = ( engine: Engine ): keyof SimulatorData | null => { - if (engine.evaluate('entreprise . auto-entrepreneur').nodeValue) { + if ( + engine.evaluate('entreprise . catégorie juridique . EI . auto-entrepreneur') + .nodeValue + ) { return 'auto-entrepreneur' } - const catégorieJuridique = engine.evaluate( - 'entreprise . catégorie juridique' - ).nodeValue if ( engine.evaluate('entreprise . catégorie juridique . SARL . unipersonnelle') @@ -57,7 +57,14 @@ const infereSimulateurRevenuFromSituation = ( ) { return 'sasu' } - if (catégorieJuridique === 'EI') { + if ( + engine.evaluate( + 'entreprise . catégorie juridique . EI . responsabilité limité' + ).nodeValue + ) { + return 'eirl' + } + if (engine.evaluate('entreprise . catégorie juridique . EI').nodeValue) { const métierProfessionLibéral = engine.evaluate( 'dirigeant . indépendant . PL . métier' ).nodeValue @@ -77,7 +84,7 @@ const infereSimulateurRevenuFromSituation = ( case 'santé . pharmacien': return 'pharmacien' } - if (engine.evaluate('dirigeant . PL').nodeValue) { + if (engine.evaluate('dirigeant . indépendant . PL').nodeValue) { return 'profession-libérale' } return 'entreprise-individuelle' @@ -101,9 +108,7 @@ export default function Gérer() { const company = useSelector( (state: RootState) => state.inFranceApp.existingCompany ) - const dirigeantSimulateur = infereSimulateurRevenuFromSituation( - useContext(EngineContext) - ) + const dirigeantSimulateur = infereSimulateurRevenuFromSituation(useEngine()) const simulateurs = useSimulatorsData() const sitePaths = useContext(SitePathsContext) if (!company) { @@ -141,7 +146,7 @@ export default function Gérer() { -

Entreprise et revenus

+

Simulateur revenus et impôts

{dirigeantSimulateur !== null && ( - ) @@ -196,12 +200,12 @@ export default function Gérer() { )} - + - + @@ -220,78 +224,38 @@ export default function Gérer() { ) } -type CompanySectionProps = { - company: Company | null +const companyDetailsConfig = { + situation: { + 'contrat salarié': 'non', + }, + objectifs: [ + 'dirigeant . régime social', + 'entreprise . imposition', + ] as DottedName[], } - -export const CompanyDetailsConversation = ({ - company, -}: CompanySectionProps) => { - const [autoEntrepreneurModal, showAutoEntrepreneurModal] = useState(false) - - const sitePaths = useContext(SitePathsContext) - const companyRef = useRef(null) - useEffect(() => { - if (companyRef.current !== company) { - companyRef.current = company - if ( - company?.statutJuridique === 'EI' && - company?.isAutoEntrepreneur == null && - !inferPLSimulateurFromCompanyDetails(company) - ) { - showAutoEntrepreneurModal(true) - } - } - }, [company]) - +export const AskCompanyMissingDetails = () => { + useSimulationConfig(companyDetailsConfig, { + useExistingCompanyFromSituation: true, + }) const dispatch = useDispatch() - const handleAnswerAutoEntrepreneur = (isAutoEntrepreneur: boolean) => { - dispatch(specifyIfAutoEntrepreneur(isAutoEntrepreneur)) - showAutoEntrepreneurModal(false) - } - - const { t } = useTranslation() - + const onChange = + (dottedName: DottedName) => (value: PublicodesExpression | undefined) => { + dispatch(updateSituation(dottedName, value)) + } + const dependencies = useObjectifsDeepDependencies() + const engine = useEngine() return ( <> - {autoEntrepreneurModal && ( + + Répondez à ces quelques questions rapides pour selectionner les outils + et assistants qui vous conviennent le mieux. + + {dependencies.map((dottedName) => ( <> - - - - - - - - - - - - - )} - - {company && ( - <> - - - - Changer l'entreprise sélectionnée - - +

{engine.getRule(dottedName).rawNode.question}

+ - )} + ))} ) } diff --git a/site/source/reducers/inFranceAppReducer.ts b/site/source/reducers/inFranceAppReducer.ts index 897b8a2491..a838bea1b1 100644 --- a/site/source/reducers/inFranceAppReducer.ts +++ b/site/source/reducers/inFranceAppReducer.ts @@ -100,6 +100,9 @@ function existingCompany( if (action.type === 'EXISTING_COMPANY::RESET') { return null } + if (action.type === 'EXISTING_COMPANY::SET_COMPANY') { + return action.entreprise + } if (state && action.type === 'EXISTING_COMPANY::ADD_COMMUNE_DETAILS') { return { ...state, localisation: action.details } } diff --git a/site/source/selectors/simulationSelectors.ts b/site/source/selectors/simulationSelectors.ts index 67b891042a..dc6b01c8d6 100644 --- a/site/source/selectors/simulationSelectors.ts +++ b/site/source/selectors/simulationSelectors.ts @@ -1,5 +1,5 @@ -import { DottedName } from 'modele-social' import { RootState, SimulationConfig, Situation } from '@/reducers/rootReducer' +import { DottedName } from 'modele-social' export const configSelector = (state: RootState): Partial => state.simulation?.config ?? {} @@ -13,7 +13,7 @@ export const objectifsSelector = (state: RootState) => { .flat() const objectifs = [...primaryObjectifs, ...(config['objectifs cachés'] ?? [])] - return objectifs + return objectifs as Array } const emptySituation: Situation = {}