diff --git a/site/cypress/integration/mon-entreprise/reduction-generale.ts b/site/cypress/integration/mon-entreprise/reduction-generale.ts index e5eefc7f3..41257db76 100755 --- a/site/cypress/integration/mon-entreprise/reduction-generale.ts +++ b/site/cypress/integration/mon-entreprise/reduction-generale.ts @@ -102,8 +102,10 @@ describe( 'td[id^="salarié___cotisations___exonérations___réduction_générale-"]' ) .should('have.length', 12) - .each(($td) => { - cy.wrap($td).should('include.text', '523,26 €') + .each(($td, $index) => { + if ($index < 10) { + cy.wrap($td).should('include.text', '493,43 €') + } }) }) @@ -115,10 +117,10 @@ describe( cy.get(inputSelector).eq(1).type('{selectall}2000') cy.get( 'td[id="salarié___cotisations___exonérations___réduction_générale-janvier"]' - ).should('include.text', '523,26 €') + ).should('include.text', '493,43 €') cy.get( 'td[id="salarié___cotisations___exonérations___réduction_générale-février"]' - ).should('include.text', '470,07 €') + ).should('include.text', '440,23 €') }) it('should save remuneration between tabs', function () { @@ -159,10 +161,10 @@ describe( ).should('include.text', '0 €') cy.get( 'td[id="salarié___cotisations___exonérations___réduction_générale-mars"]' - ).should('include.text', '523,26 €') + ).should('include.text', '493,43 €') cy.get( 'td[id="salarié___cotisations___exonérations___réduction_générale-décembre"]' - ).should('include.text', '460,38 €') + ).should('include.text', '432,49 €') }) it('should include progressive regularisation', function () { @@ -178,13 +180,13 @@ describe( ).should('include.text', '0 €') cy.get( 'td[id="salarié___cotisations___exonérations___réduction_générale__régularisation-février"]' - ).should('include.text', '-62,17 €') + ).should('include.text', '-92,12 €') cy.get( 'td[id="salarié___cotisations___exonérations___réduction_générale-mars"]' - ).should('include.text', '522,87 €') + ).should('include.text', '493,57 €') cy.get( 'td[id="salarié___cotisations___exonérations___réduction_générale-décembre"]' - ).should('include.text', '522,98 €') + ).should('include.text', '523,62 €') }) it('should be RGAA compliant', function () { 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 523a7cab3..8f126c204 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 @@ -14,6 +14,7 @@ import Simulation, { } from '@/components/Simulation' import { SimulationValue } from '@/components/Simulation/SimulationValue' import { useEngine } from '@/components/utils/EngineContext' +import useYear from '@/components/utils/useYear' import { Message } from '@/design-system' import { Spacing } from '@/design-system/layout' import { Body } from '@/design-system/typography/paragraphs' @@ -29,7 +30,6 @@ import WarningSalaireTrans from './components/WarningSalaireTrans' import RéductionGénéraleMoisParMois from './RéductionGénéraleMoisParMois' import { getInitialRéductionGénéraleMoisParMois, - getRéductionGénéraleFromRémunération, MonthState, réductionGénéraleDottedName, reevaluateRéductionGénéraleMoisParMois, @@ -112,10 +112,12 @@ function RéductionGénéraleSimulationGoals({ const dispatch = useDispatch() const { t } = useTranslation() const [réductionGénéraleMoisParMoisData, setData] = useState([]) + const year = useYear() const initializeRéductionGénéraleMoisParMoisData = useCallback(() => { - setData(getInitialRéductionGénéraleMoisParMois(engine)) - }, [engine, setData]) + const data = getInitialRéductionGénéraleMoisParMois(year, engine) + setData(data) + }, [engine, year]) useEffect(() => { if (réductionGénéraleMoisParMoisData.length === 0) { @@ -132,10 +134,11 @@ function RéductionGénéraleSimulationGoals({ return reevaluateRéductionGénéraleMoisParMois( previousData, engine, + year, régularisationMethod ) }) - }, [engine, situation, régularisationMethod]) + }, [engine, situation, régularisationMethod, year]) const updateRémunérationBruteAnnuelle = (data: MonthState[]): void => { const rémunérationBruteAnnuelle = data.reduce( @@ -160,17 +163,18 @@ function RéductionGénéraleSimulationGoals({ setData((previousData) => { const updatedData = [...previousData] updatedData[monthIndex] = { + ...updatedData[monthIndex], rémunérationBrute, - réductionGénérale: getRéductionGénéraleFromRémunération( - engine, - rémunérationBrute - ), - régularisation: 0, } updateRémunérationBruteAnnuelle(updatedData) - return updatedData + return reevaluateRéductionGénéraleMoisParMois( + updatedData, + engine, + year, + régularisationMethod + ) }) } diff --git a/site/source/pages/simulateurs/reduction-generale/utils.ts b/site/source/pages/simulateurs/reduction-generale/utils.ts index 95fb45948..ebff14cf5 100644 --- a/site/source/pages/simulateurs/reduction-generale/utils.ts +++ b/site/source/pages/simulateurs/reduction-generale/utils.ts @@ -7,24 +7,64 @@ import Engine from 'publicodes' export const rémunérationBruteDottedName = 'salarié . cotisations . assiette' export const réductionGénéraleDottedName = 'salarié . cotisations . exonérations . réduction générale' +export const heuresSupplémentairesDottedName = + 'salarié . temps de travail . heures supplémentaires' +export const heuresComplémentairesDottedName = + 'salarié . temps de travail . heures complémentaires' export type MonthState = { rémunérationBrute: number + options: Options réductionGénérale: number régularisation: number } +export type Options = { + heuresSupplémentaires?: number + heuresComplémentaires?: number +} + export type RégularisationMethod = 'annuelle' | 'progressive' -export const getRéductionGénéraleFromRémunération = ( - engine: Engine, - rémunérationBrute: number +const getDateForContexte = (monthIndex: number, year: number): string => { + const date = new Date(year, monthIndex) + + return date.toLocaleDateString('fr') +} + +const getMonthlyRéductionGénérale = ( + date: string, + rémunérationBrute: number, + options: Options, + engine: Engine ): number => { const réductionGénérale = engine.evaluate({ valeur: réductionGénéraleDottedName, unité: '€/mois', contexte: { + date, [rémunérationBruteDottedName]: rémunérationBrute, + [heuresSupplémentairesDottedName]: options.heuresSupplémentaires ?? 0, + [heuresComplémentairesDottedName]: options.heuresComplémentaires ?? 0, + }, + }) + + return réductionGénérale.nodeValue as number +} + +const getTotalRéductionGénérale = ( + rémunérationBrute: number, + SMIC: number, + coefT: number, + engine: Engine +): number => { + const réductionGénérale = engine.evaluate({ + valeur: réductionGénéraleDottedName, + arrondi: 'non', + contexte: { + [rémunérationBruteDottedName]: rémunérationBrute, + 'salarié . temps de travail . SMIC': SMIC, + 'salarié . cotisations . exonérations . T': coefT, }, }) @@ -32,6 +72,7 @@ export const getRéductionGénéraleFromRémunération = ( } export const getInitialRéductionGénéraleMoisParMois = ( + year: number, engine: Engine ): MonthState[] => { const rémunérationBrute = @@ -40,26 +81,76 @@ export const getInitialRéductionGénéraleMoisParMois = ( arrondi: 'oui', unité: '€/mois', })?.nodeValue as number) || 0 - const réductionGénérale = rémunérationBrute - ? getRéductionGénéraleFromRémunération(engine, rémunérationBrute) - : 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 - return Array(12).fill({ - rémunérationBrute, - réductionGénérale, - régularisation: 0, - }) as MonthState[] + if (!rémunérationBrute) { + return Array(12).fill({ + rémunérationBrute, + options: { + heuresSupplémentaires, + heuresComplémentaires, + }, + réductionGénérale: 0, + régularisation: 0, + }) as MonthState[] + } + + return Array.from({ length: 12 }, (_item, monthIndex) => { + const date = getDateForContexte(monthIndex, year) + + const réductionGénérale = getMonthlyRéductionGénérale( + date, + rémunérationBrute, + { + heuresSupplémentaires, + heuresComplémentaires, + }, + engine + ) + + return { + rémunérationBrute, + options: { + heuresSupplémentaires, + heuresComplémentaires, + }, + réductionGénérale, + régularisation: 0, + } + }) } export const reevaluateRéductionGénéraleMoisParMois = ( data: MonthState[], engine: Engine, + year: number, régularisationMethod: RégularisationMethod ): MonthState[] => { - const SMICMensuel = engine.evaluate({ - valeur: 'salarié . temps de travail . SMIC', - unité: 'heures/mois', - }).nodeValue as number + const totalRémunérationBrute = sumAll( + data.map((monthData) => monthData.rémunérationBrute) + ) + + if (!totalRémunérationBrute) { + return data.map((monthData) => { + return { + ...monthData, + réductionGénérale: 0, + régularisation: 0, + } + }) + } + + 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 la réduction générale, // le résultat ne sera pas bon à cause de l'assiette de cotisations du contexte const coefT = engine.evaluate({ @@ -67,43 +158,73 @@ export const reevaluateRéductionGénéraleMoisParMois = ( }).nodeValue as number const reevaluatedData = data.reduce( - (reevaluatedData: MonthState[], monthState: MonthState, index) => { + (reevaluatedData: MonthState[], monthState, monthIndex) => { const rémunérationBrute = monthState.rémunérationBrute + const options = monthState.options let réductionGénérale = 0 let régularisation = 0 - const partialData = [ - ...reevaluatedData, - { - rémunérationBrute, - réductionGénérale, - régularisation, - }, - ] + if (!rémunérationBrute) { + return [ + ...reevaluatedData, + { + rémunérationBrute, + options, + réductionGénérale, + régularisation, + }, + ] + } if (régularisationMethod === 'progressive') { - régularisation = getRégularisationProgressive( - index, - partialData, - SMICMensuel, + // La régularisation progressive du mois N est la différence entre la réduction générale + // 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 réductions générales déjà accordées (en incluant les régularisations). + const réductionGénéraleTotale = getTotalRéductionGénérale( + rémunérationBruteCumulées[monthIndex], + SMICCumulés[monthIndex], coefT, engine ) + const réductionGénéraleCumulée = sumAll( + reevaluatedData.map( + (monthData) => + monthData.réductionGénérale + monthData.régularisation + ) + ) + régularisation = réductionGénéraleTotale - réductionGénéraleCumulée + if (régularisation > 0) { - réductionGénérale += régularisation + réductionGénérale = régularisation régularisation = 0 } } else if (régularisationMethod === 'annuelle') { - réductionGénérale = getRéductionGénéraleFromRémunération( - engine, - rémunérationBrute + const date = getDateForContexte(monthIndex, year) + réductionGénérale = getMonthlyRéductionGénérale( + date, + rémunérationBrute, + options, + engine ) - if (index === data.length - 1) { - régularisation = getRégularisationAnnuelle( - partialData, - réductionGénérale, + + if (monthIndex === data.length - 1) { + // La régularisation annuelle est la différence entre la réduction générale calculée + // pour la rémunération annuelle (comparée au SMIC annuel) et la somme des réductions + // générales déjà accordées. + const réductionGénéraleTotale = getTotalRéductionGénérale( + rémunérationBruteCumulées[monthIndex], + SMICCumulés[monthIndex], + coefT, engine ) + const currentRéductionGénéraleCumulée = + réductionGénérale + + sumAll( + reevaluatedData.map((monthData) => monthData.réductionGénérale) + ) + régularisation = + réductionGénéraleTotale - currentRéductionGénéraleCumulée + if (réductionGénérale + régularisation > 0) { réductionGénérale += régularisation régularisation = 0 @@ -115,6 +236,7 @@ export const reevaluateRéductionGénéraleMoisParMois = ( ...reevaluatedData, { rémunérationBrute, + options, réductionGénérale, régularisation, }, @@ -126,67 +248,72 @@ export const reevaluateRéductionGénéraleMoisParMois = ( return reevaluatedData } -// La régularisation annuelle est la différence entre la réduction générale calculée -// pour la rémunération annuelle (comparée au SMIC annuel) et la somme des réductions -// générales déjà accordées. -const getRégularisationAnnuelle = ( +const getSMICCumulés = ( data: MonthState[], - réductionGénéraleDernierMois: number, + year: number, engine: Engine -): number => { - const currentRéductionGénéraleAnnuelle = - réductionGénéraleDernierMois + - sumAll(data.map((monthData) => monthData.réductionGénérale)) - const realRéductionGénéraleAnnuelle = engine.evaluate({ - valeur: réductionGénéraleDottedName, - arrondi: 'non', - unité: '€/an', - }).nodeValue as number +): number[] => { + return data.reduce((SMICCumulés: number[], monthData, monthIndex) => { + const SMIC = engine.evaluate({ + valeur: 'salarié . temps de travail . SMIC', + unité: '€/mois', + contexte: { + date: getDateForContexte(monthIndex, year), + [heuresSupplémentairesDottedName]: + monthData.options.heuresSupplémentaires, + [heuresComplémentairesDottedName]: + monthData.options.heuresComplémentaires, + }, + }).nodeValue as number - return realRéductionGénéraleAnnuelle - currentRéductionGénéraleAnnuelle + if (monthIndex < 1) { + return [SMIC] + } + + // S'il n'y a pas de rémunération ce mois-ci, il n'y a pas de réduction générale + // et il ne faut pas compter le SMIC de ce mois-ci dans le SMIC cumulé. + let SMICCumulé = 0 + // S'il y a une rémunération ce mois-ci, il faut aller chercher la dernière valeur + // positive du SMIC cumulé. + if (monthData.rémunérationBrute > 0) { + const previousSMICCumulé = + SMICCumulés.findLast((SMICCumulé) => SMICCumulé > 0) || 0 + SMICCumulé = previousSMICCumulé + SMIC + } + + SMICCumulés.push(SMICCumulé) + + return SMICCumulés + }, []) } -// La régularisation progressive du mois N est la différence entre la réduction générale -// calculée pour la rémunération totale jusqu'à N (comparée au SMIC mensuel * N) et la -// somme des N-1 réductions générales déjà accordées (en incluant les régularisations). -const getRégularisationProgressive = ( - monthIndex: number, - data: MonthState[], - SMICMensuel: number, - coefT: number, - engine: Engine -): number => { - if (monthIndex > data.length - 1) { - return 0 - } - const nbOfMonths = monthIndex + 1 - const partialData = data.slice(0, nbOfMonths) +const getRémunérationBruteCumulées = (data: MonthState[]) => { + return data.reduce( + (rémunérationBruteCumulées: number[], monthData, monthIndex) => { + const rémunérationBrute = monthData.rémunérationBrute - const rémunérationBruteCumulée = sumAll( - partialData.map((monthData) => monthData.rémunérationBrute) - ) + if (monthIndex < 1) { + return [rémunérationBrute] + } - if (!rémunérationBruteCumulée) { - return 0 - } + // S'il n'y a pas de rémunération ce mois-ci, il n'y a pas de réduction générale + // et elle ne compte pas non plus pour la régularisation des mois à venir. + let rémunérationBruteCumulée = 0 + // S'il y a une rémunération ce mois-ci, il faut aller chercher la dernière valeur + // positive de la rémunération cumulée. + if (rémunérationBrute > 0) { + 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 + rémunérationBrute + } - const SMICCumulé = nbOfMonths * SMICMensuel + rémunérationBruteCumulées.push(rémunérationBruteCumulée) - const réductionGénéraleTotale = engine.evaluate({ - valeur: réductionGénéraleDottedName, - arrondi: 'non', - contexte: { - [rémunérationBruteDottedName]: rémunérationBruteCumulée, - 'salarié . temps de travail . SMIC': SMICCumulé, - 'salarié . cotisations . exonérations . T': coefT, + return rémunérationBruteCumulées }, - }).nodeValue as number - - const currentRéductionGénéraleCumulée = sumAll( - partialData.map( - (monthData) => monthData.réductionGénérale + monthData.régularisation - ) + [] ) - - return réductionGénéraleTotale - currentRéductionGénéraleCumulée }