Cherchez comment se calcule une pension de retraite saoudienne et la réponse la plus partagée sera celle-ci :
Dernier salaire de base × mois de service ÷ 480
Elle a été repartagée des milliers de fois. Elle apparaît, en termes légèrement différents, dans le calculateur de retraite gratuit qui occupe actuellement la troisième position dans les résultats de recherche saoudiens. Et elle est fausse de trois manières simultanées.
Elle se trompe sur le salaire : l'Organisation générale des assurances sociales n'utilise pas le dernier salaire de base, mais la moyenne des vingt-quatre derniers salaires soumis à cotisation — et elle plafonne cette moyenne. Elle se trompe sur le diviseur : 480 ne s'applique qu'au service postérieur au 1/1/1422H, le service antérieur s'acquérant à 600. Et elle est muette sur ce qui a changé le plus récemment : les amendements à la loi sur les assurances sociales entrés en vigueur le mercredi 27/12/1445H, soit le 3 juillet 2024, qui ont fait passer l'âge légal de départ d'un nombre fixe à un barème progressif.
Chacune de ces erreurs est un bug livrable en production. Ensemble, elles font la différence entre annoncer à quelqu'un qu'il partira à soixante ans avec 5 000 riyals par mois, et lui annoncer qu'il partira à soixante et un ans et huit mois avec 4 750.
Ce tutoriel construit le moteur correctement.
Ce que vous allez construire
Un module TypeScript qui prend les dates et l'historique salarial d'un assuré et renvoie une évaluation de droits défendable :
- Classification du régime — trois populations et non deux, déterminées par deux conditions cumulatives
- Âge légal de départ — une constante hégirienne pour un régime, un barème grégorien pour un autre, 65 ans pour le troisième
- Éligibilité à la retraite anticipée — un second barème, de 300 à 360 mois
- La base salariale — moyenne plafonnée sur 24 mois, ou moyenne des 180 meilleurs mois, selon le régime
- La pension — répartie entre les taux d'acquisition 600 et 480 à la frontière du 1/1/1422H
Chaque règle est couverte par un test, et l'un de ces tests reproduit au riyal près l'exemple chiffré publié par la GOSI elle-même.
Prérequis
- Node.js 20+ et TypeScript 5+
- Une familiarité avec
Intl.DateTimeFormatet les calendriers non grégoriens - Aucun identifiant d'API GOSI — il s'agit d'un moteur de calcul pur, exécutable hors ligne
Lisez également notre tutoriel sur le moteur de cotisations GOSI si ce n'est pas déjà fait. Celui-là traite des cotisations qui entrent. Celui-ci traite des prestations qui sortent, et les deux partagent un modèle d'assuré mais presque rien d'autre.
Étape 1 : trois régimes, pas deux
L'erreur d'architecture la plus courante ici est un booléen. L'ingénieur lit que la loi a changé en juillet 2024 et écrit isNewLaw. Il y a trois populations, et c'est celle du milieu qu'un booléen ne peut pas représenter.
Le Conseil des ministres a réservé la nouvelle loi sur les assurances sociales aux nouveaux entrants sur le marché du travail sans périodes de cotisation antérieures. Les assurés existants restent sous les règles anciennes — à l'exception des dispositions relatives à l'âge légal de départ et aux périodes ouvrant droit à pension avant cet âge. Cette exception est tout le problème : elle crée un groupe intermédiaire régi simultanément par l'ancienne formule de prestations et les nouvelles règles d'âge.
Et l'appartenance à ce groupe intermédiaire n'est pas automatique. Elle exige que deux conditions soient réunies, appréciées au 3 juillet 2024 :
- l'assuré a moins de 50 années hégiriennes, et
- l'assuré compte moins de 240 mois de cotisations antérieures
Si l'une des deux fait défaut, rien ne change pour vous.
export const REFORM_DATE = new Date('2024-07-03T00:00:00Z'); // 27/12/1445H
export const ACCRUAL_SPLIT_DATE = new Date('2001-03-26T00:00:00Z'); // 1/1/1422H
export type Regime = 'legacy' | 'amended' | 'new';
export function classifyRegime(s: Subscriber): Regime {
const joinedAfterReform =
s.firstRegisteredAt.getTime() >= REFORM_DATE.getTime();
// La nouvelle loi vise les vrais nouveaux entrants. Celui qui revient
// avec du service antérieur n'en est pas un, quelle que soit la
// fraîcheur de sa dernière inscription.
if (joinedAfterReform && s.monthsAtReform === 0) return 'new';
const ageHijriAtReform = hijriYearsBetween(s.dateOfBirth, REFORM_DATE);
const covered = ageHijriAtReform < 50 && s.monthsAtReform < 240;
return covered ? 'amended' : 'legacy';
}Observez la seconde moitié du test new. Quelqu'un qui a travaillé six ans, est parti, puis s'est réinscrit en septembre 2024 possède une date d'inscription postérieure à la réforme et n'est catégoriquement pas un nouvel entrant. Ne se fonder que sur la date le bascule silencieusement vers un régime doté d'une base salariale différente et d'un âge de départ à 65 ans.
Les deux conditions, ou aucune. Un assuré de 47 ans avec 21 années de cotisations reste sous les règles anciennes. Un assuré de 51 ans avec 8 années reste sous les règles anciennes. Seul celui qui est à la fois jeune et en deçà de 240 mois bascule vers le barème.
Étape 2 : le bug de calendrier qui coûte deux ans
Sous les règles anciennes, l'âge légal de départ est de soixante ans. Presque toutes les implémentations écrivent une variante de ceci :
// FAUX — compare un âge grégorien à un seuil hégirien
const age = (Date.now() - dateOfBirth.getTime()) / (365.2425 * 86400000);
const eligible = age >= 60;L'âge ancien de soixante ans correspond à soixante années hégiriennes. Une année hégirienne compte environ 354,37 jours. Soixante d'entre elles font environ 21 262 jours, soit à peu près 58,2 années grégoriennes. Comparer un âge grégorien au nombre 60 retient donc l'assuré près de deux ans au-delà de la date à laquelle il remplissait réellement la condition.
Ce n'est pas une querelle d'arrondi : c'est la raison pour laquelle le barème réformé commence là où il commence. Lorsque la GOSI a publié le nouveau tableau, elle a exprimé les âges en années grégoriennes, et la tranche « inchangé » en haut du tableau ressort à 58, non à 60. Le résumé officiel décrit la fourchette obtenue comme allant de 58 à 65 années grégoriennes. Ces deux faits ne se réconcilient qu'une fois que l'on sait que l'ancien soixante était hégirien.
Ne convertissez donc pas. Évaluez chaque régime dans le calendrier où sa règle est écrite :
const HIJRI = new Intl.DateTimeFormat('en-u-ca-islamic-umalqura', {
year: 'numeric', month: 'numeric', day: 'numeric', timeZone: 'UTC',
});
function hijriParts(date: Date) {
const p = Object.fromEntries(
HIJRI.formatToParts(date).map((x) => [x.type, x.value]),
);
return { y: Number(p.year), m: Number(p.month), d: Number(p.day) };
}
/** Années hégiriennes révolues entre deux dates. */
export function hijriYearsBetween(from: Date, to: Date): number {
const a = hijriParts(from), b = hijriParts(to);
let years = b.y - a.y;
if (b.m < a.m || (b.m === a.m && b.d < a.d)) years -= 1;
return years;
}Utilisez précisément islamic-umalqura. C'est le calendrier Umm al-Qura, celui sur lequel fonctionne réellement l'État saoudien ; le calendrier islamic générique est une approximation tabulaire qui dérive d'un jour, ce qui suffit à déplacer un anniversaire d'un mois à l'autre.
Étape 3 : le barème de l'âge légal
Pour le régime réformé, la GOSI publie l'âge de départ en fonction de l'âge grégorien de l'assuré au 3 juillet 2024. Il augmente de quatre mois par année de jeunesse :
| Âge au 3 juillet 2024 | Âge de départ |
|---|---|
| 48,5 et plus | inchangé |
| de 48 à moins de 48,5 | 58 ans et 4 mois |
| de 47 à moins de 48 | 58 ans et 8 mois |
| de 46 à moins de 47 | 59 ans |
| de 43 à moins de 44 | 60 ans |
| de 40 à moins de 41 | 61 ans |
| de 29 à moins de 30 | 64 ans et 8 mois |
| moins de 29 | 65 ans |
Plutôt que de coder en dur une vingtaine de tranches, dérivez-les. Le tableau entier se réduit à une seule expression :
/** Renvoie l'âge légal de départ en mois entiers. */
export function amendedRetirementAgeMonths(gregorianAgeAtReform: number): number {
const a = gregorianAgeAtReform;
if (a >= 48.5) return 58 * 12; // inchangé
if (a >= 48) return 58 * 12 + 4; // 58 ans 4 mois
if (a < 29) return 65 * 12; // plancher du barème
return 704 + (47 - Math.floor(a)) * 4; // +4 mois par année de jeunesse
}Puis vérifiez la dérivation contre le tableau publié plutôt que de lui faire confiance. La constante 704 vaut 58 ans et 8 mois, la valeur de la tranche des 47 ans, et toutes les autres lignes découlent du calcul.
Étape 4 : le second barème — la retraite anticipée
La retraite anticipée a sa propre transition, et elle progresse par pas de douze mois et non de quatre. Elle dépend de la durée de cotisation au 3 juillet 2024, non de l'âge :
| Cotisations au 3 juillet 2024 | Mois requis |
|---|---|
| 240 mois et plus | 300 |
| de 19 à moins de 20 ans | 300 |
| de 18 à moins de 19 ans | 312 |
| de 15 à moins de 16 ans | 348 |
| moins de 15 ans | 360 |
export function earlyRetirementMonthsRequired(
regime: Regime,
monthsAtReform: number,
): number {
if (regime === 'legacy') return 300;
if (regime === 'new') return 360;
const yearsAtReform = Math.floor(monthsAtReform / 12);
if (yearsAtReform >= 19) return 300;
return Math.min(360, 300 + (19 - yearsAtReform) * 12);
}Le résumé de cette réforme le plus partagé sur les réseaux sociaux énonce le cas des 19 ans et plus et celui des 15 à 19 ans, puis s'arrête. Il omet le plancher : l'assuré comptant moins de quinze années à la date de la réforme a besoin de 360 mois pleins. C'est précisément la population la plus susceptible de planifier autour de cette règle et la moins susceptible d'en avoir appris la vérité.
Attention à la falaise. Un assuré comptant 18 ans et 11 mois au 3 juillet 2024 a besoin de 312 mois. Un mois de service de plus avant cette date l'aurait placé dans la tranche des 19 ans et ne lui aurait coûté que 300. Un seul mois d'inscription rétroactive déplace la ligne d'arrivée d'une année entière.
Étape 5 : la base salariale
Deux régimes, deux agrégations totalement différentes. C'est pourquoi une unique fonction averageWage() ne peut pas servir le moteur.
Les régimes ancien et réformé utilisent la moyenne des vingt-quatre derniers salaires soumis à cotisation. Mais cette moyenne est plafonnée : le règlement la limite à 150 % du salaire de cotisation au début des cinq dernières années. Ce plafond existe pour empêcher qu'un salaire soit gonflé peu avant la retraite afin de relever une pension viagère.
export function averageWageLegacy(
last24Wages: number[],
wageAtStartOfLast5Years: number,
): number {
if (last24Wages.length !== 24) {
throw new Error(`expected 24 monthly wages, received ${last24Wages.length}`);
}
const mean = last24Wages.reduce((a, b) => a + b, 0) / 24;
const cap = wageAtStartOfLast5Years * 1.5;
return Math.min(mean, cap);
}Le nouveau régime fait quelque chose de structurellement différent : il prend la moyenne des 180 mois les plus élevés de salaire assurable sur l'ensemble de la carrière.
export function averageWageNew(allMonthlyWages: number[]): number {
if (allMonthlyWages.length < 180) {
throw new Error(
`need at least 180 insurable months, received ${allMonthlyWages.length}`,
);
}
const top = [...allMonthlyWages].sort((a, b) => b - a).slice(0, 180);
return top.reduce((a, b) => a + b, 0) / 180;
}La conséquence mérite d'être énoncée clairement, car elle renverse une idée reçue sur la préparation de la retraite. Sous l'ancienne base, terminer sa carrière sur un salaire réduit — passage à temps partiel, rétrogradation volontaire — ampute directement la pension, puisque les vingt-quatre derniers mois sont la base. Sous la nouvelle base, cela ne change presque rien, car ces mois faibles n'entrent tout simplement pas dans les 180 meilleurs. Les conseils bâtis sur l'ancien régime induisent activement en erreur quiconque relève du nouveau.
Lever une exception sur un tableau de longueur incorrecte compte plus qu'il n'y paraît. Les deux fonctions calculeraient volontiers la moyenne de ce qu'on leur passe, et un export de paie qui renvoie discrètement 23 mois produit un nombre plausible et faux de quelques points de pourcentage — la catégorie de bug la plus difficile à retrouver ensuite.
Étape 6 : deux diviseurs, séparés au 1/1/1422H
La loi sur les assurances sociales promulguée par le décret royal M/33 est entrée en vigueur le 1/1/1422H, soit le 26 mars 2001. Le service est donc scindé en une période antérieure et une période postérieure, acquises à des taux différents : la période antérieure à un cinquantième du salaire moyen par an, la postérieure à un quarantième. Rapporté au mois, cela donne 600 et 480.
export function splitServiceMonths(serviceStart: Date, serviceEnd: Date) {
const total = gregorianMonthsBetween(serviceStart, serviceEnd);
if (serviceStart >= ACCRUAL_SPLIT_DATE) {
return { priorMonths: 0, subsequentMonths: total };
}
if (serviceEnd <= ACCRUAL_SPLIT_DATE) {
return { priorMonths: total, subsequentMonths: 0 };
}
const priorMonths = gregorianMonthsBetween(serviceStart, ACCRUAL_SPLIT_DATE);
return { priorMonths, subsequentMonths: total - priorMonths };
}
export function pensionLegacy(
averageWage: number,
priorMonths: number,
subsequentMonths: number,
): number {
return averageWage * (priorMonths / 600 + subsequentMonths / 480);
}
export function pensionNew(averageWage: number, contributionMonths: number): number {
return averageWage * contributionMonths * (0.0225 / 12); // 2,25 % par an
}La GOSI publie un exemple chiffré qui verrouille ce point : 60 mois de période antérieure produisant 1 000 riyals, et 180 mois de période postérieure produisant 3 750. Les deux se résolvent au même salaire moyen de 10 000, ce qui est utile à remarquer : cela signifie que l'exemple est cohérent en interne et exploitable comme jeu de test.
Mesurons maintenant le coût de l'erreur que tout le monde commet. Cette même carrière de 240 mois, calculée intégralement à 480 comme l'ordonne la formule virale :
correct : 10 000 × (60/600 + 180/480) = 4 750 SAR
naïf : 10 000 × (240/480) = 5 000 SAR
Deux cent cinquante riyals par mois. Chaque mois, pour le reste de la vie de l'assuré. L'erreur est invisible dans le résultat, car 5 000 ressemble parfaitement à une pension crédible.
Étape 7 : assembler l'évaluation
export function assessPension(s: Subscriber) {
const regime = classifyRegime(s);
const totalMonths = gregorianMonthsBetween(s.serviceStart, s.assessmentDate);
const statutoryAgeMonths =
regime === 'new' ? 65 * 12
: regime === 'amended'
? amendedRetirementAgeMonths(gregorianAgeAtReform(s.dateOfBirth))
: null; // l'ancien régime vaut 60 années HÉGIRIENNES, pas une constante
const reachedStatutoryAge =
regime === 'legacy'
? hijriYearsBetween(s.dateOfBirth, s.assessmentDate) >= 60
: gregorianMonthsBetween(s.dateOfBirth, s.assessmentDate)
>= statutoryAgeMonths!;
const earlyRequired = earlyRetirementMonthsRequired(regime, s.monthsAtReform);
const minimumForAgePension = regime === 'new' ? 180 : 120;
let entitlement: 'age_pension' | 'early_pension' | 'lump_sum_only';
if (reachedStatutoryAge && totalMonths >= minimumForAgePension) {
entitlement = 'age_pension';
} else if (totalMonths >= earlyRequired) {
entitlement = 'early_pension';
} else {
entitlement = 'lump_sum_only';
}
let monthlyPension = 0;
if (entitlement !== 'lump_sum_only') {
if (regime === 'new') {
monthlyPension = pensionNew(averageWageNew(s.allMonthlyWages!), totalMonths);
} else {
const { priorMonths, subsequentMonths } =
splitServiceMonths(s.serviceStart, s.assessmentDate);
const wage = averageWageLegacy(s.last24Wages!, s.wageAtStartOfLast5Years!);
monthlyPension = pensionLegacy(wage, priorMonths, subsequentMonths);
}
}
return { regime, statutoryAgeMonths, totalMonths,
earlyRetirementMonthsRequired: earlyRequired,
entitlement, monthlyPension };
}Renvoyer statutoryAgeMonths: null pour l'ancien régime est délibéré. Il n'existe aucune constante grégorienne correcte à y placer, et en inventer une — 58, 58,2, 60 — est la voie par laquelle le bug de calendrier revient sous la plume du prochain développeur. Une valeur nulle oblige l'appelant à se demander dans quel calendrier il se trouve.
Notez également que la classification détermine le droit, et que le droit détermine s'il y a pension ou non. Un assuré en deçà des deux seuils perçoit une indemnité en capital plutôt qu'une pension mensuelle, et un moteur qui lui renvoie un montant de pension a répondu à une question que personne n'a posée.
Tester votre implémentation
Testez contre des valeurs publiées, non contre votre propre réimplémentation du même calcul. Les trois tests qui méritent leur place :
test('the two anchor dates are the Hijri dates the law names', () => {
const f = new Intl.DateTimeFormat('en-u-ca-islamic-umalqura',
{ year: 'numeric', month: 'numeric', day: 'numeric', timeZone: 'UTC' });
assert.equal(f.format(REFORM_DATE), '12/27/1445 AH');
assert.equal(f.format(ACCRUAL_SPLIT_DATE), '1/1/1422 AH');
});
test('60 Hijri years is about 58.2 Gregorian years, not 60', () => {
const born = new Date('1960-01-01T00:00:00Z');
let d = new Date('2016-01-01T00:00:00Z');
while (hijriYearsBetween(born, d) < 60) d = new Date(d.getTime() + 86400000);
const gregorianYears = (d.getTime() - born.getTime()) / (365.2425 * 86400000);
assert.ok(gregorianYears > 58.1 && gregorianYears < 58.3);
});
test("GOSI's published worked example reproduces exactly", () => {
assert.equal(pensionLegacy(10000, 60, 0), 1000);
assert.equal(pensionLegacy(10000, 0, 180), 3750);
assert.equal(pensionLegacy(10000, 60, 180), 4750);
});Ajoutez aussi un test de propriété sur le barème : pour chaque âge de 20 à 60 par pas d'un quart d'année, l'âge de départ doit rester dans la fourchette 58 à 65 et ne jamais décroître à mesure que l'assuré rajeunit. Ce seul test attrape un décalage d'une unité dans l'arithmétique des tranches qu'aucun sondage ponctuel ne révélera.
La suite complète derrière ce tutoriel compte dix-sept tests, et les dix-sept passent avant que ce code n'approche un système de paie.
Dépannage
Les dates de départ tombent environ 1,8 an trop tard. Vous comparez un âge grégorien à 60. Voir l'étape 2.
Un assuré réinscrit se retrouve dans le mauvais régime. Votre test new ne s'appuie que sur la date d'inscription. Il doit aussi exiger zéro mois de cotisation antérieur.
La pension est trop élevée de quelques points pour les carrières longues. Vous divisez toute la carrière par 480. Scindez au 26 mars 2001.
Une promotion tardive produit une pension invraisemblable. Vous n'avez pas appliqué le plafond de 150 % contre le salaire du début des cinq dernières années.
Les dates hégiriennes sont décalées d'un jour. Vous avez utilisé le calendrier islamic au lieu d'islamic-umalqura.
Ce que ce moteur ne fait délibérément pas
Expliciter la frontière importe davantage dans du code de conformité que dans la plupart des logiciels :
- La décote de retraite anticipée du nouveau régime. La nouvelle loi permet un départ jusqu'à dix ans avant l'âge légal avec 360 mois de cotisations, moyennant une décote appliquée à la pension. Nous n'avons pas pu confirmer ces coefficients auprès d'une source primaire ; le moteur qualifie donc l'assuré et s'arrête avant d'appliquer la décote. Ne devinez pas ces valeurs.
- Les majorations pour personnes à charge, les minima d'invalidité non professionnelle et le plancher de pension minimale.
- La totalisation des périodes entre le régime de retraite de la fonction publique et les assurances sociales, qui obéit à ses propres règles de transfert.
Tout ce que vous ne pouvez pas sourcer, laissez-le de côté et signalez-le comme une lacune explicite. Un moteur de pension qui renvoie un chiffre faux avec assurance est nettement pire qu'un moteur qui renvoie une fourchette et une note.
Étapes suivantes
- La pension n'est pas la seule somme due en fin de carrière. L'indemnité de fin de service au titre des articles 84 et 85 est un droit distinct, versé par l'employeur en plus — vérifiez-la avec notre calculateur d'indemnité de fin de service.
- Pour le versant cotisations du même dossier d'assuré, voir le moteur de cotisations et de rapprochement GOSI.
- Pour ce qui quitte réellement la fiche de paie chaque mois, et pourquoi le taux suit le salarié et non l'entreprise, voir le taux de retenue GOSI expliqué.
- Si la carrière s'est terminée par un licenciement et non par un départ à la retraite, le moteur de règlement de fin de contrat chiffre ce scénario.
Conclusion
Si ce domaine produit autant de logiciels faux avec assurance, c'est que chaque règle prise isolément paraît simple. Soixante ans. Diviser par 480. Faire la moyenne de son salaire. Chacune est assez proche du vrai pour que le résultat ne semble jamais alarmant, et aucune ne survit au contact du texte réglementaire.
Les trois choses à retenir : l'âge légal sous les règles anciennes se mesure en années hégiriennes, si bien que soixante d'entre elles arrivent vers 58,2 années grégoriennes ; le service se scinde au 1/1/1422H en deux taux d'acquisition et non un seul ; et les amendements de juillet 2024 ont créé une population intermédiaire régie à la fois par d'anciennes règles de prestations et de nouvelles règles d'âge, qu'aucun booléen ne peut représenter.
Construisez la classification d'abord, l'arithmétique ensuite. L'arithmétique est la moitié facile.
Vous exploitez des systèmes de paie ou de RH en Arabie saoudite et vous n'êtes pas certain que vos montants de fin de service et de pension reflètent les réformes de 2024 ? Parlons-en — nous auditerons la logique de calcul face aux règles publiées et vous dirons où sont les écarts.