Cherchez sur Google depuis l'Arabie saoudite comment se calcule le congé maladie et vous obtiendrez deux réponses totalement différentes sur la même page de résultats. La plupart des pages citent l'article 117 du droit du travail : trente jours à plein salaire, soixante jours aux trois quarts, trente jours sans salaire — cent vingt jours par année. D'autres résultats, aussi bien classés, décrivent une échelle de cent quatre-vingts jours à plein salaire, cent quatre-vingts à demi-salaire et une année supplémentaire au quart : sept cent vingt jours.
Les deux sont exactes. Elles régissent des personnes différentes. L'article 117 régit les contrats de travail du secteur privé soumis au droit du travail. L'échelle plus longue relève des règlements des ressources humaines du secteur public, qui couvrent les fonctionnaires. Un moteur de paie qui choisit le mauvais régime n'est pas légèrement faux — il est faux d'un facteur six environ, dans le sens qui fait le plus mal.
Cette confusion est vivante dans les résultats de recherche, ce qui veut dire qu'elle est vivante dans les tableurs et les SIRH que ces résultats alimentent. Ce tutoriel construit le moteur qui ne s'y trompe pas : il refuse de calculer avant de savoir quel régime s'applique, valorise chaque jour sur la bonne base salariale, suit les tranches sur une année glissante qui ignore votre exercice comptable, achemine les accidents du travail hors de l'échelle, et refuse d'enregistrer un jour comme congé maladie sans certificat médical vérifiable.
Prérequis
Avant de commencer, assurez-vous de disposer de :
- Node.js 20+ et TypeScript 5.5+ installés
- Une aisance avec les dates, l'arithmétique entière et les unions discriminées en TypeScript
- Un système de paie ou de RH dont la fiche salarié connaît déjà le type de contrat, le salaire mensuel et les composantes de la rémunération
- L'accès à vos registres de congés et de présence — le moteur ne vaut que les données d'absence qui l'alimentent
- Aucune connaissance juridique préalable ; les règles légales qui comptent sont énoncées au moment où nous les implémentons
Si vous avez déjà suivi le moteur d'acquisition des congés annuels, les conventions de monnaie et de registre vous sembleront familières — délibérément, car les deux moteurs partagent une fiche salarié et doivent s'accorder au moment du solde de tout compte.
Ce que vous allez construire
Un module sickLeavePay qui prend un salarié, un ensemble d'absences certifiées médicalement
et une période de paie, et renvoie une ligne de bulletin ainsi qu'un registre défendable.
Concrètement :
- Une porte de régime qui classe la relation de travail avant toute autre chose et lève une erreur plutôt que de deviner
- Un résolveur de base salariale qui répond à la question laissée ouverte par l'article 117 : à quel salaire renvoie le mot « salaire »
- Une fenêtre d'année de maladie glissante ouverte par le premier jour de maladie, pas par le mois de janvier
- L'échelle à trois tranches consommée jour par jour, absences continues ou fractionnées
- Un routeur d'accidents qui envoie les accidents du travail vers la branche d'assurance sociale qui les paie réellement, sans consommer un seul jour du solde de l'article 117
- Une porte de certificat exigeant un code de congé maladie vérifiable avant tout paiement
- Un garde-fou de licenciement exposant les jours de protection restants, afin que votre processus de sortie ne rompe pas discrètement le contrat d'une personne que la loi protège encore
Étape 1 : trancher le régime avant tout le reste
Le bug le plus coûteux de ce domaine consiste à calculer la bonne formule pour la mauvaise personne. La première fonction du module n'est donc pas un calcul — c'est un refus.
/**
* Which statutory instrument governs this employment relationship.
*
* `labour-law` — a private-sector contract under نظام العمل. Article 117
* applies: 30 days full, 60 at three quarters, 30 unpaid, per sick year.
*
* `civil-service` — a government post under the public-sector human-resources
* regulations. A materially longer and differently tiered ladder applies.
* This engine does NOT implement it.
*/
export type Regime = 'labour-law' | 'civil-service';
export class RegimeNotSupportedError extends Error {
constructor(public readonly regime: Regime) {
super(
`Sick leave under the ${regime} regime is not implemented by this engine. ` +
`Article 117 of the Labour Law governs private-sector contracts only.`,
);
this.name = 'RegimeNotSupportedError';
}
}
export function assertLabourLaw(regime: Regime): asserts regime is 'labour-law' {
if (regime !== 'labour-law') throw new RegimeNotSupportedError(regime);
}Faites de regime un champ obligatoire de la fiche salarié, sans valeur par défaut. C'est par
une valeur par défaut que la mauvaise échelle finit appliquée en silence à quelques centaines
de personnes. Si vos données de référence ne portent pas aujourd'hui cette distinction, c'est
précisément le constat — remontez-le comme une erreur de qualité de données plutôt que de
laisser le moteur inventer une réponse.
Pourquoi lever une erreur plutôt que retomber sur une valeur. Un moteur de paie qui renvoie un chiffre est cru sur parole. Celui qui refuse d'en renvoyer un fait l'objet d'une enquête. Face à un écart d'un facteur six, l'enquête est l'issue la moins chère.
Étape 2 : résoudre la base salariale — c'est là que l'argent fuit
L'article 117 dit que le salarié a droit à un congé maladie « avec salaire » pour les trente premiers jours et « aux trois quarts du salaire » pour les soixante suivants. Il ne précise pas lequel.
Le droit du travail en définit deux, et vos autres moteurs utilisent déjà chacun différemment : l'indemnité de fin de service se calcule sur le salaire réel — le salaire de base plus les indemnités régulières effectivement perçues, comme le logement et le transport — tandis que les heures supplémentaires sont explicitement valorisées à partir du salaire de base majoré de cinquante pour cent. L'article 117 emploie le mot sans qualificatif, ce qui se lit comme le salaire réel. Les systèmes qui réutilisent ici la base des heures supplémentaires sous-paient chaque jour de maladie, souvent de trente à quarante pour cent, sur une ligne que personne n'audite avant le départ d'un salarié.
/** All money is integer halalas. 1 SAR = 100 halalas. Never floats. */
export type Halalas = number;
export type WageComponents = {
basic: Halalas;
housing: Halalas;
transport: Halalas;
/** Other allowances paid regularly and unconditionally, per month. */
otherRegular: Halalas;
/** Variable pay: commission, bonuses, irregular incentives. Excluded. */
variable: Halalas;
};
export type WageBase = 'actual' | 'basic';
export function monthlyWage(w: WageComponents, base: WageBase): Halalas {
if (base === 'basic') return w.basic;
// The actual wage: basic plus what is paid regularly and unconditionally.
// Variable pay is deliberately excluded — it is not part of the fixed wage.
return w.basic + w.housing + w.transport + w.otherRegular;
}
/**
* The daily rate. The Labour Law prices a month at 30 days for wage
* purposes, so this divisor is 30 regardless of how many days the
* calendar month actually has. Kept fractional on purpose — see Step 4.
*/
export function dailyRate(w: WageComponents, base: WageBase): number {
return monthlyWage(w, base) / 30;
}Notez le diviseur. Un jour de maladie en février et un jour de maladie en août coûtent la même chose, parce que le salaire est valorisé au trentième de mois et non au jour calendaire. Les systèmes qui divisent par le nombre réel de jours produisent un taux qui change d'un mois à l'autre et un solde que personne ne sait reproduire.
Faites de la base salariale une décision explicite et tracée. Si vos contrats ou votre règlement intérieur promettent mieux que la loi, c'est permis — le droit du travail fixe un plancher, pas un plafond — mais le moteur doit enregistrer qu'il a appliqué une politique au-dessus du plancher, pour qu'un inspecteur voie un choix au lieu d'en déduire un bug.
Étape 3 : l'année de maladie est glissante et démarre quand le salarié tombe malade
Voici le piège qui survit à la revue de code, parce que la version fausse a l'air évidemment juste.
L'article 117 mesure l'échelle sur « une année », et l'année en question commence à la date du premier congé maladie. Pas le 1er janvier. Pas votre exercice comptable. Pas la date anniversaire d'embauche. L'horloge démarre le premier jour où le salarié justifie sa maladie, court un an, et les tranches ne se réinitialisent que lorsqu'un jour de maladie tombe hors de cette fenêtre — auquel cas ce jour ouvre une nouvelle fenêtre.
Le découpage par année civile se trompe dans les deux sens. Un salarié malade vingt-cinq jours en décembre et vingt-cinq jours en janvier a consommé cinquante jours d'une même année de maladie et devrait déjà être dans la tranche à soixante-quinze pour cent ; le découpage calendaire paie les deux périodes à plein tarif. Inversement, un salarié dont la fenêtre s'est ouverte en mars 2025 et refermée en mars 2026 récupère en mars une tranche à plein salaire que le découpage calendaire lui refuse.
export type SickYear = {
/** Inclusive ISO date on which this window opened. */
start: string;
/** Inclusive ISO date on which it closes: start plus one year, minus one day. */
end: string;
};
const DAY_MS = 86_400_000;
function toUTC(iso: string): number {
const [y, m, d] = iso.split('-').map(Number);
return Date.UTC(y, m - 1, d);
}
function toISO(ms: number): string {
return new Date(ms).toISOString().slice(0, 10);
}
export function openSickYear(firstSickDay: string): SickYear {
const [y, m, d] = firstSickDay.split('-').map(Number);
// One year later, minus one day, so the window is inclusive on both ends.
const endMs = Date.UTC(y + 1, m - 1, d) - DAY_MS;
return { start: firstSickDay, end: toISO(endMs) };
}
export function isWithin(year: SickYear, day: string): boolean {
const t = toUTC(day);
return t >= toUTC(year.start) && t <= toUTC(year.end);
}Deux détails à fixer. Travaillez partout en minuits UTC — un moteur de paie qui respecte le
fuseau local décalera silencieusement un jour de bordure lors d'un déménagement de serveur, et
un jour de maladie qui tombe un jour plus tôt peut basculer toute une période dans une autre
tranche. Et construisez la fenêtre par arithmétique calendaire (Date.UTC(y + 1, ...)) plutôt
qu'en ajoutant 365 jours, pour que les années bissextiles ne rognent pas un jour de droit à
quelqu'un.
Étape 4 : consommer l'échelle jour par jour
L'échelle de l'article 117 s'applique « que les congés soient continus ou fractionnés ». L'unité de compte est donc le jour, et la tranche dans laquelle tombe un jour donné ne dépend que du nombre de jours de maladie déjà consommés dans la fenêtre en cours.
export type Tier = {
/** Cumulative day number, inclusive, at which this tier ends. */
throughDay: number;
numerator: number;
denominator: number;
label: string;
};
/** Article 117: 30 days full, the next 60 at three quarters, the next 30 unpaid. */
export const ARTICLE_117: readonly Tier[] = [
{ throughDay: 30, numerator: 1, denominator: 1, label: 'full pay' },
{ throughDay: 90, numerator: 3, denominator: 4, label: 'three quarters' },
{ throughDay: 120, numerator: 0, denominator: 1, label: 'unpaid' },
];
/**
* Split `days` new sick days, starting after `alreadyUsed` days consumed in
* this window, into per-tier slices. Days beyond 120 fall outside the
* entitlement entirely and are returned separately.
*/
export function splitAcrossTiers(
alreadyUsed: number,
days: number,
): { slices: { tier: Tier; days: number }[]; beyondEntitlement: number } {
const slices: { tier: Tier; days: number }[] = [];
let cursor = alreadyUsed;
let remaining = days;
for (const tier of ARTICLE_117) {
if (remaining <= 0) break;
const roomInTier = tier.throughDay - cursor;
if (roomInTier <= 0) continue;
const take = Math.min(roomInTier, remaining);
slices.push({ tier, days: take });
cursor += take;
remaining -= take;
}
return { slices, beyondEntitlement: remaining };
}Valorisez maintenant les tranches. La décision d'arrondi compte plus qu'il n'y paraît : les trois quarts d'un taux journalier tombent rarement sur un nombre entier de halalas, et arrondir chaque jour accumule une dérive de plusieurs riyals sur une période de soixante jours. Multipliez d'abord, arrondissez une seule fois par tranche.
export type PaySlice = {
tier: string;
days: number;
amount: Halalas;
};
export function priceSlices(
slices: { tier: Tier; days: number }[],
rate: number,
): PaySlice[] {
return slices.map(({ tier, days }) => ({
tier: tier.label,
days,
// Multiply across the whole slice, then round once. Rounding per day
// drifts by several riyals over a 60-day spell.
amount: Math.round((rate * days * tier.numerator) / tier.denominator),
}));
}Les jours au-delà du cent vingtième ne sont pas un « congé maladie non payé » — ils sortent
entièrement du droit ouvert par l'article 117. Traitez-les comme une absence non autorisée, sauf
si les parties conviennent d'un congé sans solde au titre de l'article 116, qui exige l'accord
de l'employeur et constitue un enregistrement différent aux conséquences différentes. Ne laissez
pas le moteur gommer la distinction : renvoyez beyondEntitlement et laissez l'appelant décider.
Étape 5 : un accident du travail n'est pas un congé maladie
Un salarié blessé au travail n'est pas en congé au titre de l'article 117. Les accidents du travail relèvent de la branche des risques professionnels de la loi sur l'assurance sociale, où l'indemnisation prend la forme d'une allocation journalière financée par le régime d'assurance et non d'un jour de maladie payé par l'employeur, à un taux fixé par la réglementation de l'assurance.
Deux conséquences en découlent, et les systèmes de paie se trompent régulièrement sur les deux. Ces jours ne doivent pas consommer l'échelle de l'article 117 — un salarié qui passe quarante jours à se remettre d'un accident du travail conserve l'intégralité de ses trente jours à plein salaire s'il tombe malade plus tard. Et ces jours ne doivent pas être payés deux fois, une fois en congé maladie par la paie et une fois en allocation via la déclaration d'accident.
export type AbsenceCause = 'illness' | 'occupational-injury';
export type Routed =
| { route: 'article-117'; days: string[] }
| { route: 'occupational-hazards'; days: string[]; note: string };
export function routeByCause(cause: AbsenceCause, days: string[]): Routed {
if (cause === 'occupational-injury') {
return {
route: 'occupational-hazards',
days,
note:
'Compensated through the occupational hazards branch of social insurance. ' +
'Does NOT consume Article 117 balance and must not be paid as sick leave.',
};
}
return { route: 'article-117', days };
}La classification est un problème de données avant d'être un problème de code. Si vos absences portent un motif en texte libre, vous ne pouvez pas router de façon fiable. Ajoutez la cause comme champ contraint au point de saisie, exigez la référence de la déclaration d'accident pour tout ce qui est classé professionnel, et rapprochez chaque mois les jours routés de vos déclarations d'assurance. Le moteur de cotisations GOSI traite déjà le versant cotisations de cette relation ; ceci en est le versant prestations, sur la même fiche salarié.
Étape 6 : pas de certificat vérifié, pas de jour de maladie payé
Le droit ouvert par l'article 117 appartient au salarié « qui justifie sa maladie ». La preuve est un rapport médical émis par un organisme reconnu, et en Arabie saoudite ces rapports sont émis et vérifiés par voie numérique — les congés maladie certifiés d'un salarié apparaissent dans la plateforme de santé Sehhaty et portent un code de service que l'employeur peut vérifier auprès de la plateforme nationale des services de santé.
Cela donne à la paie un contrôle qu'elle devrait réellement appliquer : un jour sans code vérifiable n'est pas un jour de maladie.
export type Certificate = {
/** The sick leave service code issued with the certificate. */
code: string;
from: string;
to: string;
cause: AbsenceCause;
/** Set only after checking the code against the issuing platform. */
verified: boolean;
verifiedAt?: string;
};
export class UnverifiedCertificateError extends Error {
constructor(code: string) {
super(
`Sick leave certificate ${code} has not been verified. ` +
`Days covered by it cannot be paid under Article 117.`,
);
this.name = 'UnverifiedCertificateError';
}
}
export function assertVerified(cert: Certificate): void {
if (!cert.verified) throw new UnverifiedCertificateError(cert.code);
}Stockez verifiedAt et l'identité de celui qui a effectué la vérification. Lorsqu'un solde est
contesté deux ans plus tard, « nous avons vérifié le code à cette date » est une défense ; « le
manager a dit que c'était bon » ne l'est pas.
Résistez à la tentation de vérifier automatiquement en scrapant un portail. La vérification est une action délibérée et tracée, menée par un humain ou un compte de service contre un point de terminaison officiel, et sa place est derrière votre couche d'intégration, pas dans le calcul du salaire.
Étape 7 : exposer le garde-fou de licenciement
Tant que le salarié se trouve dans le droit de cent vingt jours, la maladie n'est pas disponible pour l'employeur comme motif de rupture du contrat. Cette protection fait partie de la raison d'être de l'article 117, et c'est la règle la plus susceptible de transformer une sortie de routine en contentieux.
Un moteur qui ne renvoie que de l'argent ne suffit pas ici. Renvoyez les jours de protection restants comme un champ de premier ordre, et faites-le lire par votre processus de sortie.
export type Protection = {
daysUsed: number;
daysRemaining: number;
/** True while the statutory sick-leave entitlement is not yet exhausted. */
protected: boolean;
windowEnds: string;
};
const ENTITLEMENT_DAYS = 120;
export function protectionStatus(year: SickYear, daysUsed: number): Protection {
const daysRemaining = Math.max(0, ENTITLEMENT_DAYS - daysUsed);
return {
daysUsed,
daysRemaining,
protected: daysRemaining > 0,
windowEnds: year.end,
};
}Une règle voisine mérite d'apparaître dans le même objet : le salarié peut demander à rattacher son congé annuel à son congé maladie. Lorsque la demande est accordée, les jours rattachés sont des jours de congé annuel — payés à plein tarif, prélevés sur le solde annuel, et ne consommant pas les tranches de l'article 117. Modélisez-les comme des enregistrements de congé annuel pour qu'ils traversent le moteur d'acquisition, et laissez le moteur de congé maladie n'y voir qu'un trou dans la séquence des jours de maladie. Deux moteurs, un registre, aucun double comptage.
Étape 8 : assembler la ligne de bulletin et le registre
Tout ce qui précède se compose en un point d'entrée unique.
export type Employee = {
id: string;
regime: Regime;
wage: WageComponents;
wageBase: WageBase;
};
export type SickLeaveResult = {
employeeId: string;
period: { from: string; to: string };
year: SickYear;
slices: PaySlice[];
total: Halalas;
beyondEntitlement: number;
routedToInsurance: string[];
protection: Protection;
};
export function computeSickLeavePay(
employee: Employee,
certificates: Certificate[],
period: { from: string; to: string },
priorDaysUsed: number,
openWindow: SickYear | null,
): SickLeaveResult {
assertLabourLaw(employee.regime);
certificates.forEach(assertVerified);
const illnessDays: string[] = [];
const injuryDays: string[] = [];
for (const cert of certificates) {
const days = expandDays(cert.from, cert.to).filter(
(d) => d >= period.from && d <= period.to,
);
const routed = routeByCause(cert.cause, days);
if (routed.route === 'occupational-hazards') injuryDays.push(...routed.days);
else illnessDays.push(...routed.days);
}
illnessDays.sort();
// The window opens on the first sick day if none is open, and rolls
// forward the moment a sick day falls outside the current one.
let year = openWindow;
let used = priorDaysUsed;
if (year === null && illnessDays.length > 0) {
year = openSickYear(illnessDays[0]);
used = 0;
}
if (year !== null) {
for (const day of illnessDays) {
if (!isWithin(year, day)) {
year = openSickYear(day);
used = 0;
break;
}
}
}
if (year === null) year = openSickYear(period.from);
const inWindow = illnessDays.filter((d) => isWithin(year!, d));
const { slices, beyondEntitlement } = splitAcrossTiers(used, inWindow.length);
const priced = priceSlices(slices, dailyRate(employee.wage, employee.wageBase));
return {
employeeId: employee.id,
period,
year,
slices: priced,
total: priced.reduce((sum, s) => sum + s.amount, 0),
beyondEntitlement,
routedToInsurance: injuryDays,
protection: protectionStatus(year, used + inWindow.length),
};
}
function expandDays(from: string, to: string): string[] {
const out: string[] = [];
for (let t = toUTC(from); t <= toUTC(to); t += DAY_MS) out.push(toISO(t));
return out;
}Persistez le résultat, pas seulement le total. Les bornes de la fenêtre, la répartition par tranche, les codes de certificat et les jours routés vers l'assurance sont ce qui vous permettra de répondre à une question dans dix-huit mois sans tout recalculer contre des règles qui auront peut-être changé.
Tester votre implémentation
Les cas ci-dessous sont ceux qui attrapent de vrais bugs. Écrivez-les avant de faire confiance au module.
import { describe, expect, it } from 'vitest';
const wage: WageComponents = {
basic: 800_000, // 8,000 SAR
housing: 200_000, // 2,000 SAR
transport: 50_000, // 500 SAR
otherRegular: 0,
variable: 300_000, // commission — must be excluded
};
describe('Article 117 sick leave', () => {
it('excludes variable pay from the actual wage', () => {
expect(monthlyWage(wage, 'actual')).toBe(1_050_000);
expect(monthlyWage(wage, 'basic')).toBe(800_000);
});
it('prices the month at 30 days regardless of the calendar', () => {
// 10,500 SAR over 30 days = 350 SAR per day.
expect(dailyRate(wage, 'actual')).toBe(35_000);
});
it('splits a 100-day spell across all three tiers', () => {
const { slices, beyondEntitlement } = splitAcrossTiers(0, 100);
expect(slices.map((s) => s.days)).toEqual([30, 60, 10]);
expect(beyondEntitlement).toBe(0);
});
it('carries the tier cursor across intermittent spells', () => {
// 25 days used in December, 25 more in January of the same sick year.
const { slices } = splitAcrossTiers(25, 25);
expect(slices.map((s) => [s.tier.label, s.days])).toEqual([
['full pay', 5],
['three quarters', 20],
]);
});
it('reports days beyond the 120-day entitlement separately', () => {
const { beyondEntitlement } = splitAcrossTiers(115, 20);
expect(beyondEntitlement).toBe(15);
});
it('closes the window one year after the first sick day, inclusive', () => {
expect(openSickYear('2026-03-10')).toEqual({
start: '2026-03-10',
end: '2027-03-09',
});
});
it('handles a leap year without losing a day', () => {
expect(openSickYear('2027-03-01').end).toBe('2028-02-29');
});
it('refuses to compute for a civil-service employee', () => {
expect(() => assertLabourLaw('civil-service')).toThrow(RegimeNotSupportedError);
});
it('refuses to pay an unverified certificate', () => {
const cert: Certificate = {
code: 'GSL-000',
from: '2026-05-01',
to: '2026-05-05',
cause: 'illness',
verified: false,
};
expect(() => assertVerified(cert)).toThrow(UnverifiedCertificateError);
});
it('does not consume Article 117 balance for an occupational injury', () => {
const routed = routeByCause('occupational-injury', ['2026-06-01', '2026-06-02']);
expect(routed.route).toBe('occupational-hazards');
});
it('rounds once per slice, not once per day', () => {
// A daily rate of 333.33 SAR at three quarters over 60 days.
const odd: WageComponents = { ...wage, housing: 0, transport: 0, basic: 999_990 };
const rate = dailyRate(odd, 'actual'); // 33_333 halalas exactly
const [slice] = priceSlices([{ tier: ARTICLE_117[1], days: 60 }], rate);
expect(slice.amount).toBe(Math.round(rate * 60 * 0.75));
});
});Le test des périodes fractionnées est celui qui mérite d'être passé sur des données réelles.
Exportez une année de vos propres absences, faites-la traverser splitAcrossTiers avec un
curseur reporté, et comparez à ce que votre système actuel a payé. L'écart, s'il y en a un, se
loge presque toujours en décembre et en janvier.
Dépannage
Tous les salariés atterrissent dans la tranche à plein salaire. Votre curseur se
réinitialise. Soit vous passez priorDaysUsed à zéro à chaque exécution de paie, soit vous
ouvrez une année de maladie par année civile. La fenêtre persiste d'une exécution à l'autre —
stockez-la sur le salarié, pas dans la requête.
Le solde diverge de quelques riyals du calculateur du ministère. Presque toujours l'arrondi quotidien. Multipliez sur la tranche et arrondissez une seule fois, et vérifiez que vous divisez le salaire mensuel par trente et non par le nombre de jours du mois calendaire.
Le salaire de maladie paraît inférieur d'environ trente pour cent partout. Vous êtes sur le
salaire de base au lieu du salaire réel. Vérifiez wageBase et confirmez que le logement et le
transport alimentent bien monthlyWage.
Un salarié affiche plus de 120 jours consommés. Deux causes probables : des jours d'accident du travail enregistrés comme maladie, ou des jours de congé annuel rattachés comptés deux fois. Vérifiez d'abord le routage — c'est la plus fréquente des deux.
La frontière de tranche bouge selon l'heure d'exécution du job. Fuseau horaire. Chaque date
de ce module est un minuit UTC ; un new Date(iso) interprété en heure locale quelque part dans
votre chaîne décalera les jours de bordure.
La vérification passe pour des certificats jamais contrôlés. Quelqu'un met verified à
true par défaut. Rendez le champ obligatoire sans valeur par défaut, et stockez verifiedAt
pour qu'un enregistrement non vérifié soit visiblement incomplet plutôt que discrètement
permissif.
Prochaines étapes
- Rapprochez les jours de congé rattachés du moteur d'acquisition des congés annuels — les deux moteurs partagent un registre et ne doivent jamais compter le même jour deux fois
- Gardez les bases salariales bien distinctes de celles du moteur d'heures supplémentaires, qui utilise le salaire de base là où celui-ci utilise le salaire réel
- Routez les accidents du travail vers le versant prestations du moteur de cotisations GOSI
- Assurez-vous que les jours non payés se rapprochent du fichier de salaires que vous déposez — voyez le générateur et validateur de fichier WPS
- Laissez les salariés et les RH vérifier les droits voisins avec le calculateur des droits du travail saoudien et le calculateur de congés gratuits
Conclusion
Le congé maladie a l'air d'être la ligne la plus simple de la paie saoudienne, et c'est l'une des plus régulièrement fausses. La formule tient en une phrase, mais cette phrase cache quatre décisions que votre code doit prendre explicitement : quel régime régit le salarié, à quel salaire renvoie le mot « salaire », quand a réellement commencé l'année qui porte les tranches, et si l'absence relève seulement de l'article 117. Réglez ces quatre points et l'arithmétique devient triviale. Trompez-vous sur un seul et l'erreur devient systématique — le même chiffre faux, tous les mois, pour tous les salariés concernés, jusqu'à ce que quelqu'un parte et compte.
Si les chiffres de congé maladie de votre SIRH ne correspondent pas à ceux de votre paie, ou si personne ne peut vous dire quelle base salariale utilise le calcul actuel, dites-nous à quoi ressemble votre système — nous ferons passer une année de vos absences réelles dans un moteur comme celui-ci et vous montrerons où les deux divergent, avant qu'un contentieux prud'homal ne s'en charge.