feat: gère les heures sup dans le calcul de la RGCP (pas d'interface)

pull/3233/head
Alice Dahan 2024-11-26 13:25:11 +01:00 committed by liliced
parent 50822f1352
commit 3faa153250
3 changed files with 240 additions and 107 deletions

View File

@ -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 () {

View File

@ -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<MonthState[]>([])
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
)
})
}

View File

@ -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<DottedName>,
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<DottedName>
): 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<DottedName>
): 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<DottedName>
): 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<DottedName>,
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<DottedName>
): 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<DottedName>
): 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
}