+ onRémunérationChange(
+ index,
+ (rémunérationBrute as RémunérationBruteInput).valeur
+ )
+ }
+ value={data.rémunérationBrute}
+ formatOptions={{
+ maximumFractionDigits: 2,
+ }}
+ displaySuggestions={false}
+ />
+ )
+ }
+
+ const OptionsButton = () => {
+ return (
+
+ )
+ }
+
+ const MontantLodeom = () => {
+ return (
+
+ )
+ }
+
+ const MontantRégularisation = () => {
+ return (
+
+ )
+ }
+
+ return mobileVersion ? (
+
+ {monthName}
+
+
+
+
+
+
+
+
+
+
+
+
+ {isOptionVisible && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ <>
+
+ {monthName} |
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+ {isOptionVisible && (
+
+ |
+
+
+ |
+
+ )}
+ >
+ )
+}
+
+const StyledMonth = styled(Body)`
+ font-weight: bold;
+ text-transform: capitalize;
+ border-bottom: solid 1px ${({ theme }) => theme.colors.bases.primary[100]};
+`
+const GridContainer = styled(Grid)`
+ align-items: baseline;
+ justify-content: space-between;
+`
+const StyledBody = styled(Body)`
+ margin-top: 0;
+`
+const OptionsContainer = styled.div`
+ margin-top: ${({ theme }) => theme.spacings.xs};
+ background-color: ${({ theme }) => theme.colors.bases.primary[200]};
+ padding: ${({ theme }) => theme.spacings.sm};
+`
+
+const StyledTableRow = styled.tr`
+ background-color: ${({ theme }) => theme.colors.bases.primary[200]};
+ td {
+ padding-top: ${({ theme }) => theme.spacings.sm};
+ padding-bottom: ${({ theme }) => theme.spacings.sm};
+ }
+`
+const InputContainer = styled.div`
+ ${FlexCenter}
+ gap: ${({ theme }) => theme.spacings.md};
+`
diff --git a/site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx b/site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx
new file mode 100644
index 000000000..35e31f8f5
--- /dev/null
+++ b/site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx
@@ -0,0 +1,91 @@
+import { formatValue } from 'publicodes'
+import { useTranslation } from 'react-i18next'
+import { styled } from 'styled-components'
+
+import { Condition } from '@/components/EngineValue/Condition'
+import { FlexCenter } from '@/design-system/global-style'
+import { SearchIcon, WarningIcon } from '@/design-system/icons'
+import { Tooltip } from '@/design-system/tooltip'
+
+import {
+ rémunérationBruteDottedName,
+ Répartition as RépartitionType,
+} from '../utils'
+import Répartition from './Répartition'
+import WarningSalaireTrans from './WarningSalaireTrans'
+
+type Props = {
+ id?: string
+ rémunérationBrute: number
+ lodeom: number
+ répartition: RépartitionType
+ displayedUnit: string
+ language: string
+ displayNull?: boolean
+ alignment?: 'center' | 'end'
+}
+
+export default function MontantRéduction({
+ id,
+ rémunérationBrute,
+ lodeom,
+ répartition,
+ displayedUnit,
+ language,
+ displayNull = true,
+ alignment = 'end',
+}: Props) {
+ const { t } = useTranslation()
+
+ const tooltip =
+
+ return lodeom ? (
+
+
+ {formatValue(
+ {
+ nodeValue: lodeom,
+ },
+ {
+ displayedUnit,
+ language,
+ }
+ )}
+
+
+
+ ) : (
+ displayNull && (
+
+ {formatValue(0, { displayedUnit, language })}
+
+
+ }>
+ {t('Attention')}
+
+
+
+
+ )
+ )
+}
+
+const StyledTooltip = styled(Tooltip)`
+ width: 100%;
+`
+const FlexDiv = styled.div<{ $alignment: 'end' | 'center' }>`
+ ${FlexCenter}
+ justify-content: ${({ $alignment }) => $alignment};
+`
+const StyledSearchIcon = styled(SearchIcon)`
+ margin-left: ${({ theme }) => theme.spacings.sm};
+`
+const StyledWarningIcon = styled(WarningIcon)`
+ margin-top: ${({ theme }) => theme.spacings.xxs};
+ margin-left: ${({ theme }) => theme.spacings.sm};
+`
diff --git a/site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx b/site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx
new file mode 100644
index 000000000..12706188d
--- /dev/null
+++ b/site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx
@@ -0,0 +1,161 @@
+import { sumAll } from 'effect/Number'
+import { useTranslation } from 'react-i18next'
+import { styled } from 'styled-components'
+
+import { Grid } from '@/design-system/layout'
+import { Body } from '@/design-system/typography/paragraphs'
+
+import { MonthState } from '../utils'
+import MontantRéduction from './MontantRéduction'
+
+type Props = {
+ label: string
+ data: MonthState[]
+ mobileVersion?: boolean
+}
+
+export type RémunérationBruteInput = {
+ unité: string
+ valeur: number
+}
+
+export default function RécapitulatifTrimestre({
+ label,
+ data,
+ mobileVersion = false,
+}: Props) {
+ const { t, i18n } = useTranslation()
+ const language = i18n.language
+ const displayedUnit = '€'
+
+ const rémunération = sumAll(
+ data.map((monthData) => monthData.rémunérationBrute)
+ )
+ const répartition = {
+ IRC: sumAll(
+ data.map(
+ (monthData) =>
+ monthData.lodeom.répartition.IRC +
+ monthData.régularisation.répartition.IRC
+ )
+ ),
+ Urssaf: sumAll(
+ data.map(
+ (monthData) =>
+ monthData.lodeom.répartition.Urssaf +
+ monthData.régularisation.répartition.Urssaf
+ )
+ ),
+ }
+ let réduction = sumAll(data.map((monthData) => monthData.lodeom.value))
+ let régularisation = sumAll(
+ data.map((monthData) => monthData.régularisation.value)
+ )
+ if (réduction + régularisation > 0) {
+ réduction += régularisation
+ régularisation = 0
+ } else {
+ régularisation += réduction
+ réduction = 0
+ }
+
+ const MontantExonération = () => {
+ return (
+
+ )
+ }
+
+ const MontantRégularisation = () => {
+ return (
+
+ )
+ }
+
+ return mobileVersion ? (
+
+ {label}
+
+
+
+ {t(
+ 'pages.simulateurs.lodeom.recap.header.réduction',
+ 'Réduction calculée'
+ )}
+ {/*
+ {t(
+ 'pages.simulateurs.lodeom.recap.code671',
+ 'code 671(€)'
+ )} */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {t(
+ 'pages.simulateurs.lodeom.recap.header.régularisation',
+ 'Régularisation calculée'
+ )}
+ {/*
+ {t(
+ 'pages.simulateurs.lodeom.recap.code801',
+ 'code 801(€)'
+ )} */}
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ {label} |
+
+
+ |
+
+
+ |
+
+ )
+}
+
+const StyledMonth = styled(Body)`
+ font-weight: bold;
+ text-transform: capitalize;
+ border-bottom: solid 1px ${({ theme }) => theme.colors.bases.primary[100]};
+`
+const GridContainer = styled(Grid)`
+ align-items: center;
+ justify-content: space-between;
+`
+const StyledBody = styled(Body)`
+ margin-top: 0;
+`
diff --git a/site/source/pages/simulateurs/lodeom/components/Répartition.tsx b/site/source/pages/simulateurs/lodeom/components/Répartition.tsx
new file mode 100644
index 000000000..8f9b7767b
--- /dev/null
+++ b/site/source/pages/simulateurs/lodeom/components/Répartition.tsx
@@ -0,0 +1,58 @@
+import { Trans, useTranslation } from 'react-i18next'
+import { styled } from 'styled-components'
+
+import RépartitionValue from '@/components/RépartitionValue'
+import { Strong } from '@/design-system/typography'
+import { Li, Ul } from '@/design-system/typography/list'
+import { Body } from '@/design-system/typography/paragraphs'
+
+import { lodeomDottedName, Répartition as RépartitionType } from '../utils'
+
+type Props = {
+ répartition: RépartitionType
+}
+
+export default function Répartition({ répartition }: Props) {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
+
+ Détail du montant :
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+const StyledUl = styled(Ul)`
+ margin-top: 0;
+`
+const StyledLi = styled(Li)`
+ &::before {
+ margin-top: ${({ theme }) => theme.spacings.sm};
+ }
+`
diff --git a/site/source/pages/simulateurs/lodeom/components/WarningSalaireTrans.tsx b/site/source/pages/simulateurs/lodeom/components/WarningSalaireTrans.tsx
new file mode 100644
index 000000000..76a6e2c64
--- /dev/null
+++ b/site/source/pages/simulateurs/lodeom/components/WarningSalaireTrans.tsx
@@ -0,0 +1,11 @@
+import { Trans } from 'react-i18next'
+
+export default function WarningSalaireTrans() {
+ return (
+
+ Le barème de compétitivité concerne uniquement les salaires inférieurs à
+ 2,2 SMIC. C'est-à-dire, pour 2024, une rémunération totale qui ne dépasse
+ pas 3 964 € bruts par mois.
+
+ )
+}
diff --git a/site/source/pages/simulateurs/lodeom/components/Warnings.tsx b/site/source/pages/simulateurs/lodeom/components/Warnings.tsx
new file mode 100644
index 000000000..068980c42
--- /dev/null
+++ b/site/source/pages/simulateurs/lodeom/components/Warnings.tsx
@@ -0,0 +1,33 @@
+import { Trans } from 'react-i18next'
+
+import { Condition } from '@/components/EngineValue/Condition'
+import { Message } from '@/design-system'
+import { Body } from '@/design-system/typography/paragraphs'
+
+export default function Warnings() {
+ return (
+ <>
+
+
+
+
+ L'exonération Lodeom n'est pas cumulable avec l'exonération Jeune
+ Entreprise Innovante (JEI).
+
+
+
+
+
+
+
+
+
+ L'exonération Lodeom ne s'applique pas sur les gratifications de
+ stage.
+
+
+
+
+ >
+ )
+}
diff --git a/site/source/pages/simulateurs/lodeom/config.ts b/site/source/pages/simulateurs/lodeom/config.ts
new file mode 100644
index 000000000..034c96789
--- /dev/null
+++ b/site/source/pages/simulateurs/lodeom/config.ts
@@ -0,0 +1,31 @@
+import { config } from '../_configs/config'
+import { SimulatorsDataParams } from '../_configs/types'
+import RéductionGénéraleSimulation from './Lodeom'
+import { configRéductionGénérale } from './simulationConfig'
+
+export function lodeomConfig({ t, sitePaths }: SimulatorsDataParams) {
+ return config({
+ id: 'lodeom',
+ beta: true,
+ tracking: 'lodeom',
+ icône: '🏷️',
+ iframePath: 'simulateur-lodeom',
+ pathId: 'simulateurs.lodeom',
+ shortName: t('pages.simulateurs.lodeom.shortname', 'Éxonération Lodeom'),
+ title: t(
+ 'pages.simulateurs.lodeom.title',
+ "Simulateur d'éxonération Lodeom"
+ ),
+ meta: {
+ title: t('pages.simulateurs.lodeom.meta.title', 'Éxonération Lodeom'),
+ description: t(
+ 'pages.simulateurs.lodeom.meta.description',
+ "Estimation du montant de l'exonération Lodeom. Cette exonération est applicable, sous conditions, aux salariés d'Outre-mer."
+ ),
+ },
+ nextSteps: ['salarié'],
+ path: sitePaths.simulateurs.lodeom,
+ simulation: configRéductionGénérale,
+ component: RéductionGénéraleSimulation,
+ } as const)
+}
diff --git a/site/source/pages/simulateurs/lodeom/simulationConfig.ts b/site/source/pages/simulateurs/lodeom/simulationConfig.ts
new file mode 100644
index 000000000..312a17771
--- /dev/null
+++ b/site/source/pages/simulateurs/lodeom/simulationConfig.ts
@@ -0,0 +1,48 @@
+import { SimulationConfig } from '@/domaine/SimulationConfig'
+
+export const configRéductionGénérale: SimulationConfig = {
+ // TODO: remplacer 'salarié . cotisations . assiette' par 'salarié . rémunération . brut'
+ // lorsque cette dernière n'incluera plus les frais professionnels.
+ 'objectifs exclusifs': ['salarié . cotisations . assiette'],
+ objectifs: ['salarié . cotisations . exonérations . lodeom . montant'],
+ questions: {
+ "à l'affiche": [
+ {
+ label: 'Temps partiel',
+ dottedName: 'salarié . contrat . temps de travail . temps partiel',
+ },
+ {
+ label: 'Heures supplémentaires',
+ dottedName: 'salarié . temps de travail . heures supplémentaires',
+ },
+ {
+ label: 'Heures complémentaires',
+ dottedName: 'salarié . temps de travail . heures complémentaires',
+ },
+ {
+ label: 'DFS',
+ dottedName: 'salarié . régimes spécifiques . DFS',
+ },
+ {
+ label: 'JEI',
+ dottedName: 'salarié . cotisations . exonérations . JEI',
+ },
+ ],
+ 'liste noire': [
+ 'entreprise . salariés . effectif . seuil',
+ 'salarié . contrat . CDD . motif',
+ 'salarié . rémunération . primes . activité . base',
+ 'salarié . rémunération . avantages en nature',
+ ],
+ 'non prioritaires': ['salarié . convention collective'],
+ },
+ 'unité par défaut': '€/an',
+ situation: {
+ dirigeant: 'non',
+ 'entreprise . catégorie juridique': "''",
+ 'entreprise . imposition': 'non',
+ 'salarié . cotisations . exonérations . lodeom . zone un': "'oui'",
+ 'salarié . cotisations . exonérations . lodeom . zone un . barème compétitivité':
+ "'oui'",
+ },
+}
diff --git a/site/source/pages/simulateurs/lodeom/utils.ts b/site/source/pages/simulateurs/lodeom/utils.ts
new file mode 100644
index 000000000..e37b8f789
--- /dev/null
+++ b/site/source/pages/simulateurs/lodeom/utils.ts
@@ -0,0 +1,449 @@
+import { sumAll } from 'effect/Number'
+import { DottedName } from 'modele-social'
+import Engine from 'publicodes'
+
+import { Situation } from '@/domaine/Situation'
+
+// TODO: remplacer "salarié . cotisations . assiette" par "salarié . rémunération . brut"
+// lorsqu'elle n'incluera plus les frais professionnels.
+export const rémunérationBruteDottedName = 'salarié . cotisations . assiette'
+export const lodeomDottedName =
+ 'salarié . cotisations . exonérations . lodeom . montant'
+export const heuresSupplémentairesDottedName =
+ 'salarié . temps de travail . heures supplémentaires'
+export const heuresComplémentairesDottedName =
+ 'salarié . temps de travail . heures complémentaires'
+
+export type Répartition = {
+ IRC: number
+ Urssaf: number
+}
+
+export type MonthState = {
+ rémunérationBrute: number
+ options: Options
+ lodeom: {
+ value: number
+ répartition: Répartition
+ }
+ régularisation: {
+ value: number
+ répartition: Répartition
+ }
+}
+
+export type Options = {
+ heuresSupplémentaires: number
+ heuresComplémentaires: number
+ rémunérationETP: number
+ rémunérationPrimes: number
+ rémunérationHeuresSup: number
+}
+
+export type RégularisationMethod = 'annuelle' | 'progressive'
+
+const getDateForContexte = (monthIndex: number, year: number): string => {
+ const date = new Date(year, monthIndex)
+
+ return date.toLocaleDateString('fr')
+}
+
+const getMonthlyLodeom = (
+ date: string,
+ rémunérationBrute: number,
+ options: Options,
+ engine: Engine
+): number => {
+ const lodeom = engine.evaluate({
+ valeur: lodeomDottedName,
+ unité: '€/mois',
+ contexte: {
+ date,
+ [rémunérationBruteDottedName]: rémunérationBrute,
+ [heuresSupplémentairesDottedName]: options.heuresSupplémentaires,
+ [heuresComplémentairesDottedName]: options.heuresComplémentaires,
+ },
+ })
+
+ return lodeom.nodeValue as number
+}
+
+const getTotalLodeom = (
+ rémunérationBrute: number,
+ SMIC: number,
+ coefT: number,
+ engine: Engine
+): number => {
+ const lodeom = engine.evaluate({
+ valeur: lodeomDottedName,
+ arrondi: 'non',
+ contexte: {
+ [rémunérationBruteDottedName]: rémunérationBrute,
+ 'salarié . temps de travail . SMIC': SMIC,
+ 'salarié . cotisations . exonérations . T': coefT,
+ },
+ })
+
+ return lodeom.nodeValue as number
+}
+
+const emptyRépartition = {
+ IRC: 0,
+ Urssaf: 0,
+}
+
+const getRépartition = (
+ rémunération: number,
+ réduction: number,
+ engine: Engine
+): Répartition => {
+ const contexte = {
+ [rémunérationBruteDottedName]: rémunération,
+ [lodeomDottedName]: réduction,
+ }
+ const IRC =
+ (engine.evaluate({
+ valeur: `${lodeomDottedName} . imputation retraite complémentaire`,
+ unité: '€/mois',
+ contexte,
+ })?.nodeValue as number) ?? 0
+ const Urssaf =
+ (engine.evaluate({
+ valeur: `${lodeomDottedName} . imputation sécurité sociale`,
+ unité: '€/mois',
+ contexte,
+ })?.nodeValue as number) ?? 0
+
+ return {
+ IRC,
+ Urssaf,
+ }
+}
+
+export const getInitialLodeomMoisParMois = (
+ year: number,
+ engine: Engine
+): MonthState[] => {
+ const rémunérationBrute =
+ (engine.evaluate({
+ valeur: rémunérationBruteDottedName,
+ arrondi: 'oui',
+ unité: '€/mois',
+ })?.nodeValue as number) ?? 0
+ const heuresSupplémentaires =
+ (engine.evaluate({
+ valeur: heuresSupplémentairesDottedName,
+ unité: 'heures/mois',
+ })?.nodeValue as number) ?? 0
+ const heuresComplémentaires =
+ (engine.evaluate({
+ valeur: heuresComplémentairesDottedName,
+ unité: 'heures/mois',
+ })?.nodeValue as number) ?? 0
+ const rémunérationETP = 0
+ const rémunérationPrimes = 0
+ const rémunérationHeuresSup = 0
+
+ if (!rémunérationBrute) {
+ return Array(12).fill({
+ rémunérationBrute,
+ options: {
+ heuresSupplémentaires,
+ heuresComplémentaires,
+ rémunérationETP,
+ rémunérationPrimes,
+ rémunérationHeuresSup,
+ },
+ lodeom: {
+ value: 0,
+ répartition: emptyRépartition,
+ },
+ régularisation: {
+ value: 0,
+ répartition: emptyRépartition,
+ },
+ }) as MonthState[]
+ }
+
+ return Array.from({ length: 12 }, (_item, monthIndex) => {
+ const date = getDateForContexte(monthIndex, year)
+
+ const lodeom = getMonthlyLodeom(
+ date,
+ rémunérationBrute,
+ {
+ heuresSupplémentaires,
+ heuresComplémentaires,
+ rémunérationETP,
+ rémunérationPrimes,
+ rémunérationHeuresSup,
+ },
+ engine
+ )
+ const répartition = getRépartition(rémunérationBrute, lodeom, engine)
+
+ return {
+ rémunérationBrute,
+ options: {
+ heuresSupplémentaires,
+ heuresComplémentaires,
+ rémunérationETP,
+ rémunérationPrimes,
+ rémunérationHeuresSup,
+ },
+ lodeom: {
+ value: lodeom,
+ répartition,
+ },
+ régularisation: {
+ value: 0,
+ répartition: emptyRépartition,
+ },
+ }
+ })
+}
+
+export const reevaluateLodeomMoisParMois = (
+ data: MonthState[],
+ engine: Engine,
+ year: number,
+ régularisationMethod: RégularisationMethod
+): MonthState[] => {
+ const totalRémunérationBrute = sumAll(
+ data.map((monthData) => monthData.rémunérationBrute)
+ )
+
+ if (!totalRémunérationBrute) {
+ return data.map((monthData) => {
+ return {
+ ...monthData,
+ lodeom: {
+ value: 0,
+ répartition: emptyRépartition,
+ },
+ régularisation: {
+ value: 0,
+ répartition: emptyRépartition,
+ },
+ }
+ })
+ }
+
+ const rémunérationBruteCumulées = getRémunérationBruteCumulées(data)
+ const SMICCumulés = getSMICCumulés(data, year, engine)
+ // Si on laisse l'engine calculer T dans le calcul de l'exonération Lodeom,
+ // le résultat ne sera pas bon à cause de l'assiette de cotisations du contexte
+ const coefT = engine.evaluate({
+ valeur: 'salarié . cotisations . exonérations . T',
+ }).nodeValue as number
+
+ const reevaluatedData = data.reduce(
+ (reevaluatedData: MonthState[], monthState, monthIndex) => {
+ const rémunérationBrute = monthState.rémunérationBrute
+ const options = monthState.options
+ const lodeom = {
+ value: 0,
+ répartition: emptyRépartition,
+ }
+ const régularisation = {
+ value: 0,
+ répartition: emptyRépartition,
+ }
+
+ if (!rémunérationBrute) {
+ return [
+ ...reevaluatedData,
+ {
+ rémunérationBrute,
+ options,
+ lodeom,
+ régularisation,
+ },
+ ]
+ }
+
+ if (régularisationMethod === 'progressive') {
+ // La régularisation progressive du mois N est la différence entre l'exonération Lodeom
+ // calculée pour la rémunération totale jusqu'à N (comparée au SMIC équivalent pour ces N mois)
+ // et la somme des N-1 exonérations déjà accordées (en incluant les régularisations).
+ const lodeomTotale = getTotalLodeom(
+ rémunérationBruteCumulées[monthIndex],
+ SMICCumulés[monthIndex],
+ coefT,
+ engine
+ )
+ const lodeomCumulée = sumAll(
+ reevaluatedData.map(
+ (monthData) =>
+ monthData.lodeom.value + monthData.régularisation.value
+ )
+ )
+ régularisation.value = lodeomTotale - lodeomCumulée
+
+ if (régularisation.value > 0) {
+ lodeom.value = régularisation.value
+ lodeom.répartition = getRépartition(
+ rémunérationBrute,
+ lodeom.value,
+ engine
+ )
+ régularisation.value = 0
+ } else if (régularisation.value < 0) {
+ régularisation.répartition = getRépartition(
+ rémunérationBrute,
+ régularisation.value,
+ engine
+ )
+ }
+ } else if (régularisationMethod === 'annuelle') {
+ const date = getDateForContexte(monthIndex, year)
+ lodeom.value = getMonthlyLodeom(
+ date,
+ rémunérationBrute,
+ options,
+ engine
+ )
+
+ if (monthIndex === data.length - 1) {
+ // La régularisation annuelle est la différence entre l'exonération Lodeom calculée
+ // pour la rémunération annuelle (comparée au SMIC annuel) et la somme des exonérations
+ // Lodeom déjà accordées.
+ const lodeomTotale = getTotalLodeom(
+ rémunérationBruteCumulées[monthIndex],
+ SMICCumulés[monthIndex],
+ coefT,
+ engine
+ )
+ const currentLodeomCumulée =
+ lodeom.value +
+ sumAll(reevaluatedData.map((monthData) => monthData.lodeom.value))
+ régularisation.value = lodeomTotale - currentLodeomCumulée
+
+ if (lodeom.value + régularisation.value > 0) {
+ lodeom.value += régularisation.value
+ lodeom.répartition = getRépartition(
+ rémunérationBrute,
+ lodeom.value,
+ engine
+ )
+ régularisation.value = 0
+ } else if (régularisation.value < 0) {
+ régularisation.répartition = getRépartition(
+ rémunérationBrute,
+ régularisation.value,
+ engine
+ )
+ }
+ }
+ }
+
+ return [
+ ...reevaluatedData,
+ {
+ rémunérationBrute,
+ options,
+ lodeom,
+ régularisation,
+ },
+ ]
+ },
+ []
+ )
+
+ return reevaluatedData
+}
+
+const getSMICCumulés = (
+ data: MonthState[],
+ year: number,
+ engine: Engine
+): number[] => {
+ return data.reduce((SMICCumulés: number[], monthData, monthIndex) => {
+ // S'il n'y a pas de rémunération ce mois-ci, il n'y a pas d'exonération Lodeom
+ // et il ne faut pas compter le SMIC de ce mois-ci dans le SMIC cumulé.
+ if (!monthData.rémunérationBrute) {
+ SMICCumulés.push(0)
+
+ return SMICCumulés
+ }
+
+ const contexte = {
+ date: getDateForContexte(monthIndex, year),
+ } as Situation
+
+ if (!monthData.options.rémunérationETP) {
+ contexte[heuresSupplémentairesDottedName] =
+ monthData.options.heuresSupplémentaires
+ contexte[heuresComplémentairesDottedName] =
+ monthData.options.heuresComplémentaires
+ }
+
+ let SMIC = engine.evaluate({
+ valeur: 'salarié . temps de travail . SMIC',
+ unité: '€/mois',
+ contexte,
+ }).nodeValue as number
+
+ if (monthData.options.rémunérationETP) {
+ const SMICHoraire = engine.evaluate({
+ valeur: 'SMIC . horaire',
+ contexte,
+ }).nodeValue as number
+ // On retranche les primes et le paiements des heures supplémentaires à la rémunération versée
+ // et on la compare à la rémunération équivalente "mois complet" sans les primes
+ const prorata =
+ (monthData.rémunérationBrute -
+ monthData.options.rémunérationPrimes -
+ monthData.options.rémunérationHeuresSup) /
+ monthData.options.rémunérationETP
+ // On applique ce prorata au SMIC mensuel et on y ajoute les heures supplémentaires et complémentaires
+ SMIC =
+ SMIC * prorata +
+ SMICHoraire *
+ (monthData.options.heuresSupplémentaires +
+ monthData.options.heuresComplémentaires)
+ }
+
+ let SMICCumulé = SMIC
+ if (monthIndex > 0) {
+ // Il faut aller chercher la dernière valeur positive du SMIC cumulé.
+ const previousSMICCumulé =
+ SMICCumulés.findLast((SMICCumulé) => SMICCumulé > 0) ?? 0
+ SMICCumulé = previousSMICCumulé + SMIC
+ }
+
+ SMICCumulés.push(SMICCumulé)
+
+ return SMICCumulés
+ }, [])
+}
+
+const getRémunérationBruteCumulées = (data: MonthState[]) => {
+ return data.reduce(
+ (rémunérationBruteCumulées: number[], monthData, monthIndex) => {
+ // S'il n'y a pas de rémunération ce mois-ci, il n'y a pas d'exonération Lodeom
+ // et elle ne compte pas non plus pour la régularisation des mois à venir.
+ if (!monthData.rémunérationBrute) {
+ rémunérationBruteCumulées.push(0)
+
+ return rémunérationBruteCumulées
+ }
+
+ let rémunérationBruteCumulée = monthData.rémunérationBrute
+ if (monthIndex > 0) {
+ // Il faut aller chercher la dernière valeur positive de la rémunération cumulée.
+ const previousRémunérationBruteCumulée =
+ rémunérationBruteCumulées.findLast(
+ (rémunérationBruteCumulée) => rémunérationBruteCumulée > 0
+ ) ?? 0
+ rémunérationBruteCumulée =
+ previousRémunérationBruteCumulée + monthData.rémunérationBrute
+ }
+
+ rémunérationBruteCumulées.push(rémunérationBruteCumulée)
+
+ return rémunérationBruteCumulées
+ },
+ []
+ )
+}
diff --git a/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx b/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx
index 8e8ee7636..bc92ef371 100644
--- a/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx
+++ b/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx
@@ -2,13 +2,13 @@ import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PeriodSwitch from '@/components/PeriodSwitch'
+import RégularisationSwitch from '@/components/RégularisationSwitch'
import { SelectSimulationYear } from '@/components/SelectSimulationYear'
import SimulateurWarning from '@/components/SimulateurWarning'
import Simulation from '@/components/Simulation'
import CongésPayésSwitch from './components/CongésPayésSwitch'
import EffectifSwitch from './components/EffectifSwitch'
-import RégularisationSwitch from './components/RégularisationSwitch'
import RéductionGénéraleSimulationGoals from './Goals'
import { RégularisationMethod } from './utils'
diff --git a/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx b/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx
index 562525e33..233662ea7 100644
--- a/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx
+++ b/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import NumberInput from '@/components/conversation/NumberInput'
+import MonthOptions from '@/components/MonthOptions'
import RuleLink from '@/components/RuleLink'
import { useEngine } from '@/components/utils/EngineContext'
import { Button } from '@/design-system/buttons'
@@ -19,7 +20,6 @@ import {
rémunérationBruteDottedName,
} from '../utils'
import MontantRéduction from './MontantRéduction'
-import MonthOptions from './MonthOptions'
type Props = {
monthName: string
diff --git a/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx b/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx
index d95811508..8006a5f62 100644
--- a/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx
+++ b/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx
@@ -1,6 +1,7 @@
import { Trans, useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
+import RépartitionValue from '@/components/RépartitionValue'
import { Strong } from '@/design-system/typography'
import { Li, Ul } from '@/design-system/typography/list'
import { Body } from '@/design-system/typography/paragraphs'
@@ -9,7 +10,6 @@ import {
réductionGénéraleDottedName,
Répartition as RépartitionType,
} from '../utils'
-import RépartitionValue from './RépartitionValue'
type Props = {
répartition: RépartitionType
diff --git a/site/source/pages/simulateurs/reduction-generale/components/WarningSalaireTrans.tsx b/site/source/pages/simulateurs/reduction-generale/components/WarningSalaireTrans.tsx
index e72b38ac8..7b0430f3a 100644
--- a/site/source/pages/simulateurs/reduction-generale/components/WarningSalaireTrans.tsx
+++ b/site/source/pages/simulateurs/reduction-generale/components/WarningSalaireTrans.tsx
@@ -5,7 +5,7 @@ export default function WarningSalaireTrans() {
La RGCP concerne uniquement les salaires inférieurs à 1,6 SMIC.
C'est-à-dire, pour 2024, une rémunération totale qui ne dépasse pas{' '}
- 2 827,07 € bruts par mois.
+ 2 827,07 € bruts par mois.
)
}
diff --git a/site/source/pages/statistiques/_components/SimulateursChoice.tsx b/site/source/pages/statistiques/_components/SimulateursChoice.tsx
index a3ceb52c0..eccb7dc53 100644
--- a/site/source/pages/statistiques/_components/SimulateursChoice.tsx
+++ b/site/source/pages/statistiques/_components/SimulateursChoice.tsx
@@ -1,9 +1,9 @@
+import { SimulateurCard } from '@/components/SimulateurCard'
import { Item } from '@/design-system'
import { Emoji } from '@/design-system/emoji'
import { Select } from '@/design-system/field/Select'
import useSimulatorsData from '@/hooks/useSimulatorsData'
-import { SimulateurCard } from '../../../components/SimulateurCard'
import { getFilter } from '../StatsPage'
import { Filter } from '../types'
diff --git a/site/source/sitePaths.ts b/site/source/sitePaths.ts
index 1558067a1..e08f230d8 100644
--- a/site/source/sitePaths.ts
+++ b/site/source/sitePaths.ts
@@ -73,6 +73,7 @@ const rawSitePathsFr = {
is: 'impot-societe',
dividendes: 'dividendes',
'réduction-générale': 'réduction-générale',
+ lodeom: 'lodeom',
},
nouveautés: {
index: 'nouveautés',
@@ -172,6 +173,7 @@ const rawSitePathsEn = {
is: 'corporate-tax',
dividendes: 'dividends',
'réduction-générale': 'réduction-générale',
+ lodeom: 'lodeom',
},
nouveautés: {
index: 'news',