الكتابات/tutorial/2026/11
● Tutorial9 نوفمبر 2026·22 دقيقة

توطين المهن الفنية الهندسية بنسبة 35%: محرك امتثال بلغة TypeScript

ابنِ محرك الامتثال لقرار توطين المهن الفنية الهندسية قبل ارتفاع النسبة من 30% إلى 35% في 9 مايو 2027: مراحل النسبة المؤرخة، وقاعدة النسبة الأعلى بين القرارات، واعتماد الهيئة السعودية للمهندسين، والحد الأدنى للأجر 5,000 ريال في التأمينات، وقاعدة العمل الفعلي، وآخر موعد للتسجيل الذي تفرضه مهلة التحديث خمسة أيام عمل.

أعلنت وزارة الموارد البشرية والتنمية الاجتماعية بالشراكة مع وزارة البلديات والإسكان، في 8 نوفمبر 2026، رفع نسبة توطين 94 مهنة فنية هندسية من 30% إلى 35% ابتداءً من 9 مايو 2027. يسري القرار على كل منشأة في القطاع الخاص يعمل بها 5 عاملين فأكثر في هذه المهن، ويُحتسب على مستوى كيان المنشأة. يغطي خبرنا عن القرار ما الذي تغيّر. أما هذا الدرس فموجّه لمن عليه أن يُخرج الرقم: مطوّر أنظمة الموارد البشرية في شركة مقاولات أو صيانة أو مكتب هندسي.

يبدو الحساب بسيطًا، لكنه ليس كذلك لأربعة أسباب، كل واحد منها قد يكلّفك موظفًا أو اثنين: الفني السعودي لا يُحتسب إلا إذا اتفق نظامان خارجيان، والقرار يتبع العمل الفعلي لا المسمى الوظيفي وحده، والتعيين الذي تخطط له في مايو يجب أن يُسجَّل في أبريل، وعبارة "توظيف 11" في مثال الوزارة نفسها لا تعني ما سيفهمه مسؤول التوظيف.

المتطلبات المسبقة

  • Node.js بالإصدار 20 أو أحدث، وTypeScript 5 في الوضع الصارم
  • ملف تصدير للقوى العاملة يتضمن رموز المهن كما هي مسجلة في منصة قوى، والجنسية، والأجر الخاضع للاشتراك المسجل في التأمينات الاجتماعية
  • حالة الاعتماد المهني لدى الهيئة السعودية للمهندسين لكل فني سعودي
  • الدليل الإجرائي الحالي للقرار من وزارة الموارد البشرية. عند تحققنا في 9 نوفمبر 2026 كان دليل 2026 معلنًا لكنه غير متاح للتنزيل بعد، لذلك فالقواعد أدناه مأخوذة من دليل 2025 الذي حدد نسبة 30%. أعد قراءة الدليل الجديد قبل الاعتماد على أي منها.

ما الذي ستبنيه

محركًا صغيرًا، نحو 190 سطرًا، يجيب عن خمسة أسئلة في أي تاريخ:

  1. ما النسبة السارية: 30% حتى 8 مايو 2027، و35% ابتداءً من 9 مايو 2027
  2. أي قرار يحكم كل رمز مهنة عندما يغطيه أكثر من قرار
  3. من يُحتسب فعلًا من فنييك السعوديين، وكل سبب لعدم احتساب الآخرين
  4. كم سعوديًا تحتاج، بالإحلال وبالتوظيف الصافي، وهما رقمان مختلفان
  5. آخر تاريخ يجب أن يُسجَّل فيه أي تغيير حتى يظهر في احتساب الوزارة في يوم معيّن

يعيد المحرك استخدام تصميم محرك نسبة توطين المهن المحاسبية ومحرك منافذ الصالات الرياضية، لذلك نحيل إليهما حين تكون الخطوة مشروحة هناك بالتفصيل بدل تكرارها.

الخطوة 1: ما الذي يقوله الدليل فعلًا

يقع الدليل الإجرائي لعام 2025 الخاص بالمهن الفنية الهندسية في ثماني صفحات. هذه هي الأجزاء التي تتحول إلى كود:

  • النسبة هي نسبة مئوية من إجمالي العاملين في المهن المستهدفة في المنشأة. كانت 30% ابتداءً من 23 يوليو 2025 وفق الجدول الزمني في الدليل (وذكر إعلان الوزارة 27 يوليو)، وتصبح 35% في 9 مايو 2027.
  • النطاق كل منشأة في القطاع الخاص يعمل بها 5 عاملين فأكثر في المهن المستهدفة.
  • الاعتماد المهني من الهيئة السعودية للمهندسين شرط، ولا يُحتسب الفنيون غير المعتمدين ضمن نسب التوطين.
  • الحد الأدنى للأجر هو أجر شهري مسجل في التأمينات الاجتماعية لا يقل عن 5,000 ريال. الفني السعودي الذي يتقاضى أقل من ذلك لا يُحتسب.
  • النسبة الأعلى تُطبَّق. إذا كانت مهنة فنية هندسية مستهدفة أيضًا بقرار توطين آخر بنسبة مختلفة، يُطبَّق الدليل النسبة الأعلى.
  • المسمى والعمل الفعلي. تقول الأسئلة الشائعة إن القرار يُطبَّق على المسميات المهنية وعلى العمل الفعلي للعامل. وإسناد مهام المهن المستهدفة إلى عامل غير سعودي، بطريقة مباشرة أو غير مباشرة، تحت أي مسمى آخر يُعد مخالفة.
  • التقريب إلى أقرب عدد صحيح، كما في المثال المحسوب في الدليل.
  • المتابعة آلية. تقول الأسئلة الشائعة إن النظام يطابق بيانات التأمينات الاجتماعية مع المسميات المهنية والأجور والاعتماد المهني، وإن تحديثات التأمينات والهيئة السعودية للمهندسين تنعكس على النظام خلال خمسة أيام عمل كحد أقصى.

وهناك إجابة أخرى في الأسئلة الشائعة لها أثر مباشر: المنشأة التي جميع فنييها سعوديون يجب أن تستوفي الشروط أيضًا. ومثال الدليل منشأة لديها 20 فنيًا كلهم سعوديون، يجب أن يستوفي 6 منهم على الأقل شرطي الأجر والاعتماد.

الخطوة 2: القرارات بوصفها بيانات

تعامل مع كل قرار توطين مهني كسجلّ له مراحل مؤرخة، لا كثابت في الكود. ستضيف القرار التالي، والمرحلة التالية من هذا القرار، دون أن تلمس المنطق.

/** A dated rate step: from this date (inclusive), this percentage applies. */
export interface RateStep {
  readonly from: string; // ISO date, YYYY-MM-DD
  readonly ratePercent: number; // integer, e.g. 35
}
 
export interface OccupationDecision {
  readonly id: string;
  /** Unified Saudi Classification of Occupations codes covered by the decision. */
  readonly codes: ReadonlySet<string>;
  /** Steps in date order. */
  readonly steps: readonly RateStep[];
  /** The decision applies only at or above this many workers in the covered codes. */
  readonly minWorkers: number;
  /** Minimum GOSI contributory wage, in SAR, for a Saudi to be counted. */
  readonly minWageSar: number;
}
 
export const ENGINEERING_TECHNICIANS: OccupationDecision = {
  id: 'engineering-technicians',
  // Seven of the codes printed in the 2025 procedural guide. Load the full list
  // from the current guide; do not ship this sample as your production table.
  codes: new Set([
    '311201', // civil engineering technician
    '311211', // surveying technician
    '311301', // electrical engineering technician
    '311409', // electronics engineering technician
    '311501', // mechanical engineering technician
    '311511', // mechanical maintenance technician
    '311805', // architectural draughtsman
  ]),
  steps: [
    { from: '2025-07-23', ratePercent: 30 },
    { from: '2027-05-09', ratePercent: 35 },
  ],
  minWorkers: 5,
  minWageSar: 5000,
};
 
/** The rate in force on a date, or null before the first step. */
export function rateOn(decision: OccupationDecision, isoDate: string): number | null {
  let rate: number | null = null;
  for (const step of decision.steps) {
    if (step.from <= isoDate) rate = step.ratePercent;
  }
  return rate;
}

الرموز السبعة هي المهن السبع التي سمّاها إعلان الوزارة، كما وردت في جدول دليل 2025. أما الجدول الحقيقي ففيه 94 مهنة. حمّله من الدليل لا من هذا المقال، واحفظه في نظام إدارة النسخ مع تاريخ الدليل بجانبه، لأن المراجعة القادمة ستغيّره.

التواريخ نصوص بصيغة ISO حتى تُرتَّب بالمقارنة النصية البسيطة. لا مناطق زمنية، ولا تحليل لكائنات Date، ولا خطأ بيوم واحد عند منتصف الليل بتوقيت الرياض.

الخطوة 3: عندما يغطي قراران رمزًا واحدًا

نادرًا ما تخضع شركة مقاولات لقرار واحد. للمهندسين قرارهم، ولإدارة المشاريع قرار آخر، وقد يظهر الرمز في أكثر من جدول. يحسم الدليل التداخل بقاعدة واحدة: تُطبَّق النسبة الأعلى. وفي الكود يعني ذلك أن كل عامل ينتمي في تاريخ معين إلى قرار واحد فقط، هو القرار ذو النسبة الأعلى الذي يغطي رمزه.

/**
 * Where one code is covered by several decisions, the guide applies the higher rate.
 * Returns the decision that governs this code on this date, or null if none does.
 */
export function governingDecision(
  code: string,
  isoDate: string,
  decisions: readonly OccupationDecision[],
): OccupationDecision | null {
  let best: OccupationDecision | null = null;
  let bestRate = -1;
  for (const d of decisions) {
    if (!d.codes.has(code)) continue;
    const rate = rateOn(d, isoDate);
    if (rate !== null && rate > bestRate) {
      best = d;
      bestRate = rate;
    }
  }
  return best;
}

لهذا نتيجة تستحق الذكر: قد ينتقل عامل من مجموعة إلى أخرى في يوم ارتفاع النسبة دون أي تغيير في شركتك. وعندما تحسب عدة قرارات، احسبها جميعًا بالتاريخ نفسه وقائمة الموظفين نفسها، وإلا فستحتسب بعض الأشخاص مرتين وتُسقط آخرين.

الخطوة 4: من يُحتسب من السعوديين

لا يُحتسب الفني السعودي إلا إذا تحققت ثلاثة أمور معًا: الجنسية، وأجر خاضع للاشتراك في التأمينات لا يقل عن 5,000 ريال، واعتماد ساري المفعول من الهيئة السعودية للمهندسين. الأول يأتي من نظام الموارد البشرية لديك، والثاني من التأمينات الاجتماعية، والثالث من الهيئة. ولوحة الموارد البشرية لديك لا تعرف عادةً إلا الأول.

export interface Worker {
  readonly id: string;
  readonly occupationCode: string;
  readonly saudi: boolean;
  /** Contributory wage registered with GOSI, in SAR. Not gross pay. */
  readonly gosiWageSar: number;
  /** Active professional accreditation with the Saudi Council of Engineers. */
  readonly sceAccredited: boolean;
  /**
   * True when a line manager confirms the person does targeted technical work
   * under a title outside the list. The decision follows the actual work.
   */
  readonly doesTargetedWork?: boolean;
}
 
export type Exclusion = 'NOT_SAUDI' | 'WAGE_BELOW_FLOOR' | 'NOT_SCE_ACCREDITED';
 
/** Every reason this worker does not count toward the numerator. Empty means countable. */
export function exclusions(w: Worker, decision: OccupationDecision): Exclusion[] {
  const reasons: Exclusion[] = [];
  if (!w.saudi) reasons.push('NOT_SAUDI');
  if (w.gosiWageSar < decision.minWageSar) reasons.push('WAGE_BELOW_FLOOR');
  if (!w.sceAccredited) reasons.push('NOT_SCE_ACCREDITED');
  return reasons;
}

أعِد كل الأسباب، لا السبب الأول فقط. الفني الذي يقل أجره عن الحد ولا يملك اعتمادًا يحتاج إلى إصلاحين، وإصلاح الاعتماد يستغرق وقتًا أطول. إذا أبلغت عن مشكلة الأجر وحدها فسيرفع أحدهم الأجر ثم يكتشف المشكلة الثانية في مايو.

الأجر المقصود هو الأجر الخاضع للاشتراك المسجل في التأمينات الاجتماعية، لا إجمالي الراتب في كشف الرواتب. ويختلف الرقمان أكثر مما يتوقع الناس. يوضح محرك اشتراكات التأمينات أين يحدث ذلك.

الحقل doesTargetedWork هو طريقة المحرك في تطبيق قاعدة العمل الفعلي. لن يخبرك أي ملف تصدير أن عاملًا غير سعودي مسجلًا كمشرف موقع يقضي يومه في عمل فني مساحة. وحده المدير المباشر يعرف ذلك. اجمع الإجابة مرة كل ربع سنة، واحفظها، ودع المحرك يُدخل هؤلاء في النطاق ويعرضهم في قائمة مستقلة.

الخطوة 5: التقريب دون أعداد عشرية

يضرب مثال الدليل 51 فنيًا في 30% فيحصل على 15.3 ويقرّبها إلى 15. في الحساب العشري، ناتج 51 * 0.35 هو 17.849999999999998، وهو يُقرَّب صحيحًا هنا بالصدفة، لكن مدخلات أخرى قد لا تكون بهذا اللطف. احفظ النسبة كعدد صحيح وقرّب بالأعداد الصحيحة.

/**
 * Required Saudis = workers x rate, rounded to the nearest whole number.
 * Integer arithmetic only; a half rounds up (the guide does not say, this is our reading).
 */
export function requiredSaudis(workers: number, ratePercent: number): number {
  return Math.floor((workers * ratePercent + 50) / 100);
}

لا يحدد الدليل ما يحدث عند النصف بالضبط. نحن نقرّب النصف إلى الأعلى، وهي القراءة الأحوط، والكود يصرّح بذلك. إن نص الدليل الجديد على غير ذلك، فهذا هو السطر الوحيد الذي يتغير. ويشرح درس محرك المهن المحاسبية خطأ الأعداد العشرية بتفصيل أكبر.

الخطوة 6: تقييم المنشأة

الآن نجمع القطع. يعيد التقييم رقمي الفجوة كليهما، والسعوديين المستبعدين مع أسبابهم، والأشخاص الذين أدخلتهم قاعدة العمل الفعلي.

export interface Assessment {
  readonly decisionId: string;
  readonly isoDate: string;
  readonly applies: boolean;
  readonly ratePercent: number;
  readonly workers: number;
  readonly countableSaudis: number;
  readonly required: number;
  /** Non-Saudis to replace with countable Saudis. Denominator unchanged. */
  readonly byReplacement: number;
  /** Net new countable Saudi hires if nobody leaves. Denominator grows. */
  readonly byHiringOnly: number;
  /** Saudis in scope who do not count, with every reason. */
  readonly excluded: ReadonlyArray<{ id: string; reasons: Exclusion[] }>;
  /** People counted in scope only because of the actual-work rule. */
  readonly actualWorkFlags: readonly string[];
}
 
export function hiresOnly(workers: number, countable: number, ratePercent: number): number {
  if (ratePercent >= 100) throw new Error('rate must be under 100%');
  let k = 0;
  while (countable + k < requiredSaudis(workers + k, ratePercent)) k++;
  return k;
}
 
export function assess(
  staff: readonly Worker[],
  decision: OccupationDecision,
  all: readonly OccupationDecision[],
  isoDate: string,
): Assessment {
  const rate = rateOn(decision, isoDate) ?? 0;
  const inScope: Worker[] = [];
  const actualWorkFlags: string[] = [];
  for (const w of staff) {
    if (governingDecision(w.occupationCode, isoDate, all) === decision) {
      inScope.push(w);
    } else if (w.doesTargetedWork && !w.saudi) {
      inScope.push(w);
      actualWorkFlags.push(w.id);
    }
  }
  const workers = inScope.length;
  const applies = rate > 0 && workers >= decision.minWorkers;
  const excluded: { id: string; reasons: Exclusion[] }[] = [];
  let countable = 0;
  for (const w of inScope) {
    const reasons = exclusions(w, decision);
    if (reasons.length === 0) countable++;
    else if (w.saudi) excluded.push({ id: w.id, reasons });
  }
  const required = applies ? requiredSaudis(workers, rate) : 0;
  return {
    decisionId: decision.id,
    isoDate,
    applies,
    ratePercent: rate,
    workers,
    countableSaudis: countable,
    required,
    byReplacement: Math.max(0, required - countable),
    byHiringOnly: applies ? hiresOnly(workers, countable, rate) : 0,
    excluded,
    actualWorkFlags,
  };
}

اختياران في هذه الدالة يستحقان التوضيح.

المقام يشمل كل عامل داخل النطاق، سعوديًا كان أو غير سعودي، محتسبًا أو غير محتسب. هذا ما يقتضيه مثال الدليل عن المنشأة التي جميع فنييها سعوديون: 20 فنيًا كلهم سعوديون يحتاجون إلى 6 مستوفين للشروط. لو أُخرج السعوديون غير المستوفين من المقام لاختلفت الإجابة.

الحد الأدنى للعاملين يستخدم النطاق نفسه. خمسة عاملين في المهن المستهدفة، بعد تطبيق قاعدة التداخل وقاعدة العمل الفعلي. فالشركة التي لديها أربعة فنيين وموظف واحد بمسمى مهندس يؤدي عمل فني، لديها خمسة.

الخطوة 7: "توظيف 11" لا تعني توظيف 11

مثال الدليل: 51 فنيًا، منهم 4 سعوديون، بنسبة 30%. المطلوب 15، لذلك يقول الدليل إن على المنشأة توظيف 11 فنيًا سعوديًا إضافيًا.

هذا صحيح إذا حلّ كل واحد من الأحد عشر محل عامل غير سعودي، فيبقى الإجمالي 51. وهو خطأ إذا أُضيفوا فوق العدد الحالي. مع 62 فنيًا يصبح المطلوب 18.6، أي 19 بعد التقريب، ولا يكفي 15 سعوديًا. بالتوظيف وحده تحتاج المنشأة إلى 16 سعوديًا جديدًا محتسبًا للامتثال بنسبة 30%.

وبنسبة 35% تحتاج المنشأة نفسها إلى 18 سعوديًا من 51: 14 بالإحلال، أو 21 بالتوظيف وحده.

النسبةالمطلوب من 51بالإحلالبالتوظيف وحده
30%151116
35%181421

اعرض الرقمين في أي لوحة متابعة، مع تسميتهما في النوع نفسه. فعبارة "الفجوة: 14" إذا قُرئت كهدف توظيف تترك مدير المشروع ناقصًا سبعة أشخاص في 9 مايو. ومعظم الخطط الحقيقية مزيج: إحلال عند انتهاء بعض العقود، وتوظيف صافٍ لمشاريع جديدة. والرقمان يحدّان الإجابة من الطرفين.

الخطوة 8: آخر موعد للتسجيل

تحتسب الوزارة من بيانات التأمينات والهيئة، ويقول الدليل إن التحديثات تنعكس خلال خمسة أيام عمل كحد أقصى. فالفني السعودي الذي يُعيَّن يوم الخميس 6 مايو 2027، أو يُعتمد في ذلك اليوم، قد لا يظهر في الاحتساب يوم الأحد 9 مايو. خطّط بالعكس.

/** Saudi working week: Sunday to Thursday. Friday (5) and Saturday (6) are off. */
function isWorkingDay(d: Date): boolean {
  const day = d.getUTCDay();
  return day !== 5 && day !== 6;
}
 
/**
 * Latest date a hire, wage change or accreditation must be registered so that it
 * shows in the ministry's count by `isoDate`, given a sync lag in working days.
 * The guide says updates show within five working days at most.
 */
export function registerBy(isoDate: string, lagWorkingDays = 5): string {
  const d = new Date(`${isoDate}T00:00:00Z`);
  let left = lagWorkingDays;
  while (left > 0) {
    d.setUTCDate(d.getUTCDate() - 1);
    if (isWorkingDay(d)) left--;
  }
  return d.toISOString().slice(0, 10);
}

مع أسبوع العمل السعودي من الأحد إلى الخميس، يكون اليوم الواقع قبل الأحد 9 مايو 2027 بخمسة أيام عمل هو الأحد 2 مايو 2027. هذا آخر يوم آمن لتسجيل التعيين الجديد في التأمينات، ورفع الأجر فوق 5,000 ريال، وتفعيل الاعتماد. والاعتماد نفسه يستغرق أكثر من خمسة أيام، لذا فالموعد الحقيقي لحديثي التخرج يسبق ذلك بأسابيع. اسأل الهيئة عن مدة المعالجة الحالية واطرحها أيضًا.

اختبار التنفيذ

شغّل الاختبارات بالأمر npx tsx --test engine.test.ts. تثبّت الاختبارات أرقام الدليل نفسه، وحالة المنشأة التي جميع فنييها سعوديون، والحد الأدنى للعاملين، وقاعدة التداخل، وحساب التواريخ، وتتحقق من خاصيتين ثابتتين على كل منشأة من 5 إلى 300 فني.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
  ENGINEERING_TECHNICIANS as ET, assess, hiresOnly, rateOn,
  registerBy, requiredSaudis, type OccupationDecision, type Worker,
} from './engine.ts';
 
function crew(n: number, saudi: number, code = '311201'): Worker[] {
  return Array.from({ length: n }, (_, i) => ({
    id: `w${i}`, occupationCode: code, saudi: i < saudi,
    gosiWageSar: 6000, sceAccredited: true,
  }));
}
 
test('rate steps from 30% to 35% on 9 May 2027', () => {
  assert.equal(rateOn(ET, '2025-07-22'), null);
  assert.equal(rateOn(ET, '2027-05-08'), 30);
  assert.equal(rateOn(ET, '2027-05-09'), 35);
});
 
test("the guide's worked example: 51 technicians, 4 Saudi, at 30%", () => {
  const a = assess(crew(51, 4), ET, [ET], '2026-11-09');
  assert.equal(a.required, 15);
  assert.equal(a.byReplacement, 11);
  assert.equal(a.byHiringOnly, 16);
});
 
test('the same establishment at 35%', () => {
  const a = assess(crew(51, 4), ET, [ET], '2027-05-09');
  assert.equal(a.required, 18);
  assert.equal(a.byReplacement, 14);
  assert.equal(a.byHiringOnly, 21);
});
 
test('all-Saudi FAQ: 20 Saudis still need 6 who meet both conditions', () => {
  const staff = crew(20, 20).map((w, i) =>
    i < 6 ? w : { ...w, sceAccredited: false });
  const a = assess(staff, ET, [ET], '2026-11-09');
  assert.equal(a.required, 6);
  assert.equal(a.byReplacement, 0);
  assert.equal(a.excluded.length, 14);
});
 
test('four workers in scope: the decision does not apply', () => {
  const a = assess(crew(4, 0), ET, [ET], '2027-05-09');
  assert.equal(a.applies, false);
  assert.equal(a.byHiringOnly, 0);
});
 
test('every failing reason is kept, not just the first', () => {
  const [w] = crew(1, 1);
  const a = assess(
    [{ ...w!, gosiWageSar: 4800, sceAccredited: false }, ...crew(5, 0).map((x) => ({ ...x, id: `n${x.id}` }))],
    ET, [ET], '2026-11-09');
  assert.deepEqual(a.excluded[0]?.reasons, ['WAGE_BELOW_FLOOR', 'NOT_SCE_ACCREDITED']);
});
 
test('wage floor is inclusive at SAR 5,000', () => {
  const staff = crew(10, 3).map((w, i) => (i === 0 ? { ...w, gosiWageSar: 5000 } : w));
  assert.equal(assess(staff, ET, [ET], '2026-11-09').countableSaudis, 3);
});
 
test('actual-work rule pulls an off-list non-Saudi into scope', () => {
  const staff: Worker[] = [
    ...crew(5, 2),
    { id: 'x', occupationCode: '999999', saudi: false, gosiWageSar: 3000,
      sceAccredited: false, doesTargetedWork: true },
  ];
  const a = assess(staff, ET, [ET], '2026-11-09');
  assert.equal(a.workers, 6);
  assert.deepEqual(a.actualWorkFlags, ['x']);
});
 
test('higher rate wins when two decisions cover one code', () => {
  const other: OccupationDecision = {
    id: 'other', codes: new Set(['311201']),
    steps: [{ from: '2025-01-01', ratePercent: 50 }], minWorkers: 1, minWageSar: 4000,
  };
  const all = [ET, other];
  assert.equal(assess(crew(10, 0), ET, all, '2026-11-09').workers, 0);
  assert.equal(assess(crew(10, 0), other, all, '2026-11-09').workers, 10);
});
 
test('rounding: half rounds up, .3 rounds down, no float drift', () => {
  assert.equal(requiredSaudis(10, 35), 4); // 3.5
  assert.equal(requiredSaudis(51, 30), 15); // 15.3
  assert.equal(requiredSaudis(51, 35), 18); // 17.85
  assert.equal(requiredSaudis(3, 30), 1); // 0.9
});
 
test('hiring-only never beats replacement, and replacement always complies', () => {
  for (let n = 5; n <= 300; n++) {
    for (let s = 0; s <= n; s++) {
      for (const r of [30, 35]) {
        const req = requiredSaudis(n, r);
        const rep = Math.max(0, req - s);
        assert.ok(hiresOnly(n, s, r) >= rep);
        assert.ok(s + rep >= requiredSaudis(n, r));
      }
    }
  }
});
 
test('register by: five Saudi working days before Sunday 9 May 2027', () => {
  assert.equal(registerBy('2027-05-09'), '2027-05-02');
  assert.equal(registerBy('2027-05-09', 0), '2027-05-09');
});

اختبار الخصائص الأخير هو الذي يستحق مكانه. فهو يتحقق، عبر نحو 90,000 تركيبة من الحجم وعدد السعوديين والنسبة، من أن التوظيف وحده لا يحتاج أبدًا إلى عدد أقل من الإحلال، وأن رقم الإحلال يجعل المنشأة ممتثلة دائمًا. وأي خطأ بواحد في التقريب يُفشله فورًا.

استكشاف الأخطاء وإصلاحها

يُظهر المحرك عددًا من السعوديين المحتسبين أقل مما تتوقعه الموارد البشرية. اطبع excluded. الأسباب المعتادة اعتماد انتهى عند التجديد، وأجر مسجل في التأمينات أقل بقليل من 5,000 ريال بينما يدفع كشف الرواتب أكثر.

عدد العاملين لا يطابق لوحة قوى. تحقق من قاعدة التداخل أولًا. فالرمز الذي يغطيه قرار بنسبة أعلى يخرج من هذه المجموعة. ثم تحقق من actualWorkFlags: هؤلاء داخل النطاق لأن المدير قال ذلك، وقوى لا تعرف.

دليل 2026 يغيّر قاعدة. كل قاعدة في مكان واحد: سجل القرار، أو requiredSaudis، أو exclusions. غيّر السجل أو الدالة، وستخبرك الاختبارات أي أمثلة الدليل تغيرت.

أقل من خمسة عاملين، لا شيء يُطبَّق. هذا صحيح، لكنه هش. تعيينان قد يُدخلان الشركة في النطاق. شغّل التقييم على خطة التوظيف، لا على الموظفين الحاليين فقط.

الخطوات التالية

الخلاصة

تفصلنا ستة أشهر عن نسبة 35%، وهي تكفي لدورة توظيف واحدة إن عرفت الفجوة الحقيقية الآن. يعطيك المحرك هذه الفجوة بصيغتين صادقتين، ويسمّي كل سعودي لا يُحتسب وسبب ذلك، ويتبع العمل الفعلي لا المسمى، ويحوّل 9 مايو إلى التاريخ المهم فعلًا: 2 مايو، مطروحًا منه مدة اعتماد الهيئة.

إن كنت تفضّل أن يعمل هذا الفحص تلقائيًا على بيانات قوى والتأمينات والاعتماد لديك بدل إعادة بنائه يدويًا، تواصل معنا. سننظر أين توجد هذه المصادر الثلاثة في شركتك، وأين تتوقف عن الاتفاق.