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

توطين مهنة مشرف مسكن: أداة تحقق بلغة TypeScript

القرار الوزاري 62298 يقصر مهنة مشرف مسكن (رمز 515102) على السعوديين في كل منشأة تشغّل سكنًا جماعيًا لعشرين مستفيدًا أو أكثر، مع حد أدنى للأجر المسجل في التأمينات قدره 5,000 ريال. ابنِ أداة التحقق بلغة TypeScript: نطاق المواقع، والمسمى مقابل العمل الفعلي، والحد الأدنى للأجر، وخطة عمل جاهزة للموارد البشرية.

منذ 1 فبراير 2026 أصبح العمل في مهنة مشرف مسكن في المملكة العربية السعودية مقصورًا على السعوديين. القرار الوزاري رقم 62298 بتاريخ 31 أكتوبر 2025، الصادر عن وزارة الموارد البشرية والتنمية الاجتماعية بالشراكة مع وزارة البلديات والإسكان، يحدد نسبة التوطين بـ 100% للمهنة ذات الرمز 515102. ويسري على كل منشأة تدير أو تشغّل سكنًا جماعيًا لعشرين مستفيدًا أو أكثر. ولا يُحتسب المشرف السعودي إلا إذا كان أجره الشهري المسجل في التأمينات الاجتماعية 5,000 ريال على الأقل.

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

المتطلبات

  • Node.js 20 أو أحدث وTypeScript 5 مع تفعيل strict
  • تصدير لبيانات العاملين من نظام الموارد البشرية: الجنسية، ورمز المهنة في قوى، والأجر الخاضع للاشتراك في التأمينات
  • قائمة بمواقع السكن وعدد الساكنين في كل منها
  • معرفة بطريقة عمل قرارات التوطين المهني. إن كان هذا أول قرار تتعامل معه، فاقرأ أولًا درس محرك توطين منافذ الصالات الرياضية

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

وحدة صغيرة بلا اعتماديات تجيب عن أربعة أسئلة في أي تاريخ:

  1. هل المنشأة مشمولة بالقرار أصلًا، وعن طريق أي مواقع؟
  2. من هو مشرف المسكن في نظر القرار: بالمسمى، أو بالعمل الفعلي، أو بكليهما؟
  3. ما المشكلة لدى كل واحد منهم: الجنسية، أو الأجر، أو المسمى المسجل فقط؟
  4. ما المطلوب من الموارد البشرية: كم سعوديًا يجب توظيفه، ومن يجب سحب المهام منه، وكم يجب إضافته إلى الأجور المسجلة، وأي المسميات يجب تصحيحها في قوى؟

تتبع الشيفرة التصميم نفسه المستخدم في محرك توطين المهن الفنية الهندسية: القرار بيانات، وكل قاعدة دالة صغيرة، والاختبارات تستخدم حدود الدليل نفسها.

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

قرأنا القرار ودليله الإجرائي على موقع الوزارة. هذه هي الوقائع التي تعتمد عليها الشيفرة:

القاعدةالمصدر
مشرف مسكن، رمز 515102، مقصورة على السعوديين بنسبة 100%الدليل 3.1 والجدول 1
يسري على المنشآت التي تدير أو تشغّل سكنًا جماعيًا لعشرين مستفيدًا أو أكثر في سكن مشترك: المجمعات السكنية، والكبائن المتنقلة، والمباني السكنيةالدليل 3.2
لا يُحتسب السعودي إلا بأجر شهري خاضع للاشتراك في التأمينات لا يقل عن 5,000 ريالالدليل 3.3
وجود مشرف أجره أقل من الحد الأدنى مخالفة، وليس مجرد عدم احتسابالدليل، الأسئلة الشائعة
صدر في 31 أكتوبر 2025، مع فترة سماح ثلاثة أشهر، ويُطبَّق من 1 فبراير 2026الدليل 3.5
يُطبَّق على المسميات المهنية وعلى العمل الفعلي للعاملالدليل، الأسئلة الشائعة
إسناد أي من مهام المهنة إلى غير سعودي، بصورة مباشرة أو غير مباشرة، تحت أي مسمى آخر، مخالفةالدليل 5
العقوبات وفق القرارين الوزاريين 75913 (19/05/1445) و44558 (03/04/1446)الدليل 5 والمادة الرابعة من القرار
يُطبَّق بغض النظر عن نطاق المنشأة في نطاقاتالدليل، الأسئلة الشائعة

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

الخطوة 2: القرار على شكل بيانات

كل ما يحدده الدليل يوضع في سجل واحد. وإذا عدّلت الوزارة الحد الأدنى للأجر، فإنك تغيّر رقمًا واحدًا.

/** Ministerial Decision 62298 of 31 October 2025 and its procedural guide. */
export const HOUSING_SUPERVISOR = {
  decision: '62298',
  issued: '2025-10-31',
  enforcedFrom: '2026-02-01', // issue date plus a three-month grace period
  occupationCode: '515102', // "Housing supervisor" (مشرف مسكن)
  saudiRatePercent: 100,
  minGosiWageSar: 5000, // contributory wage registered with GOSI
  minBeneficiaries: 20, // collective housing for 20 or more people
} as const;
 
/** One collective housing site: a compound, a building, a set of caravans. */
export interface HousingSite {
  readonly id: string;
  readonly beneficiaries: number; // people housed there today
}
 
export interface Worker {
  readonly id: string;
  readonly saudi: boolean;
  readonly occupationCode: string; // as registered in Qiwa
  readonly gosiWageSar: number; // integer riyals, as registered with GOSI
  /** Sites this person supervises in practice, whatever the job title says. */
  readonly supervisesSiteIds: readonly string[];
}

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

الخطوة 3: أي المواقع تجعل المنشأة مشمولة

/** Sites that bring the establishment under the decision. */
export function sitesInScope(sites: readonly HousingSite[]): HousingSite[] {
  return sites.filter((s) => s.beneficiaries >= HOUSING_SUPERVISOR.minBeneficiaries);
}
 
/** On or after the enforcement date? ISO dates compare correctly as strings. */
export function isEnforced(onDate: string): boolean {
  return onDate >= HOUSING_SUPERVISOR.enforcedFrom;
}

لاحظ المقارنة >=. الدليل يقول عشرون أو أكثر، فالموقع الذي يسكنه عشرون شخصًا بالضبط مشمول. هذا بالضبط خطأ الفرق بواحد الذي يقع فيه التنفيذ المتسرع، وأول اختبار يتحقق منه.

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

الخطوة 4: المسمى، أو العمل الفعلي، أو كلاهما

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

export type RoleSource = 'title' | 'actual-work' | 'both';
 
/** Is this person a housing supervisor for the decision, and on what evidence? */
export function supervisorRole(w: Worker): RoleSource | null {
  const byTitle = w.occupationCode === HOUSING_SUPERVISOR.occupationCode;
  const byWork = w.supervisesSiteIds.length > 0;
  if (byTitle && byWork) return 'both';
  if (byTitle) return 'title';
  if (byWork) return 'actual-work';
  return null;
}
 
export type Finding =
  | { kind: 'non-saudi'; workerId: string; role: RoleSource }
  | { kind: 'wage-below-floor'; workerId: string; shortfallSar: number }
  | { kind: 'title-mismatch'; workerId: string };
 
/** Everything wrong with one worker. An empty list means this person is fine. */
export function checkWorker(w: Worker): Finding[] {
  const role = supervisorRole(w);
  if (role === null) return [];
  if (!w.saudi) return [{ kind: 'non-saudi', workerId: w.id, role }];
  const out: Finding[] = [];
  const shortfall = HOUSING_SUPERVISOR.minGosiWageSar - w.gosiWageSar;
  if (shortfall > 0) out.push({ kind: 'wage-below-floor', workerId: w.id, shortfallSar: shortfall });
  if (role === 'actual-work') out.push({ kind: 'title-mismatch', workerId: w.id });
  return out;
}

النتائج الثلاث تقابل ثلاث محادثات مختلفة مع الموارد البشرية:

  • غير سعودي في المهنة مخالفة مهما كان مسماه. "منسق المخيم" أو "إداري الموقع" الذي يؤدي عمل المشرف هو الحالة التي يذكرها الدليل في البند 5.
  • سعودي بأجر أقل من 5,000 ريال مخالفة أيضًا. الأسئلة الشائعة تجيب عن ذلك صراحة. ويُسجَّل الفرق بالريال في النتيجة، لأنه الرقم الذي يحتاجه قسم الرواتب.
  • سعودي يؤدي العمل تحت مسمى آخر ليس مخالفة لقاعدة 100%، لكن قوى لن تحتسبه مشرف مسكن، فتختلف الصورة الرسمية عن الواقع. والحل تعديل المسمى، لا توظيف جديد.

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

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

export interface Assessment {
  readonly onDate: string;
  readonly inScope: boolean;
  readonly enforced: boolean;
  readonly sitesInScope: readonly string[];
  readonly findings: readonly Finding[];
  /** In-scope sites that no compliant Saudi supervises. Information, not a breach. */
  readonly sitesWithoutCompliantSupervisor: readonly string[];
  readonly compliant: boolean;
}
 
export function assess(
  onDate: string,
  sites: readonly HousingSite[],
  workers: readonly Worker[],
): Assessment {
  const scoped = sitesInScope(sites).map((s) => s.id);
  const inScope = scoped.length > 0;
  const findings = inScope ? workers.flatMap(checkWorker) : [];
 
  const breached = new Set(
    findings.filter((f) => f.kind !== 'title-mismatch').map((f) => f.workerId),
  );
  // A non-Saudi supervisor always has a finding, so "not breached" means a compliant Saudi.
  const covered = new Set(
    workers.filter((w) => !breached.has(w.id)).flatMap((w) => w.supervisesSiteIds),
  );
 
  return {
    onDate,
    inScope,
    enforced: isEnforced(onDate),
    sitesInScope: scoped,
    findings,
    sitesWithoutCompliantSupervisor: scoped.filter((id) => !covered.has(id)),
    compliant: findings.every((f) => f.kind === 'title-mismatch'),
  };
}

ثلاثة قرارات في التصميم تستحق الشرح.

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

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

sitesWithoutCompliantSupervisor معلومة وليست مخالفة. الدليل لا يشترط مشرفًا لكل موقع. لكن موقعًا كبيرًا لا يؤدي فيه أحد ملتزم هذا العمل يعني غالبًا أن شخصًا غير ملتزم يؤديه، وهذا ما تبحث عنه. تعامل مع القائمة كسؤال توجهه لمدير الموقع.

الخطوة 6: من النتائج إلى خطة عمل

الموارد البشرية لا تتحرك بناءً على نتائج. تتحرك بناءً على عدد توظيف، ورقم رواتب، وقائمة أسماء.

export interface ActionPlan {
  /** Saudi supervisors to recruit, one per non-Saudi doing the job today. */
  readonly saudiHires: number;
  /** Non-Saudis whose supervision duties must stop, whatever their title. */
  readonly dutiesToReassign: readonly string[];
  /** Monthly increase in registered GOSI wages, in riyals. */
  readonly wageTopUpSarPerMonth: number;
  /** Saudis to re-register in Qiwa under code 515102. */
  readonly qiwaTitleChanges: readonly string[];
}
 
export function actionPlan(a: Assessment): ActionPlan {
  const nonSaudi = a.findings.filter((f) => f.kind === 'non-saudi');
  let topUp = 0;
  for (const f of a.findings) if (f.kind === 'wage-below-floor') topUp += f.shortfallSar;
  return {
    saudiHires: nonSaudi.length,
    dutiesToReassign: nonSaudi.map((f) => f.workerId),
    wageTopUpSarPerMonth: topUp,
    qiwaTitleChanges: a.findings
      .filter((f) => f.kind === 'title-mismatch')
      .map((f) => f.workerId),
  };
}

كل غير سعودي في المهنة يحتاج إلى سعودي يتسلم العمل، ويجب أن تتوقف مهامه الإشرافية، حتى لو بقي في وظيفته بدور مختلف حقًا. وزيادة الأجر هي الزيادة الشهرية في الأجور المسجلة التي توصل كل مشرف سعودي إلى 5,000 ريال. وترتفع معها اشتراكات صاحب العمل والعامل في التأمينات، فمرّر الرقم لمن يتولى حساب الاشتراكات.

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

تستخدم الاختبارات حدود الدليل عند أطرافها: 19 و20 ساكنًا، و4,999 و5,000 ريال، و31 يناير و1 فبراير 2026.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { actionPlan, assess, checkWorker, type HousingSite, type Worker } from './engine.ts';
 
const sites: HousingSite[] = [
  { id: 'jubail-a', beneficiaries: 240 },
  { id: 'jubail-b', beneficiaries: 20 },
  { id: 'dammam-villa', beneficiaries: 19 },
];
 
function w(id: string, over: Partial<Worker> = {}): Worker {
  return { id, saudi: true, occupationCode: '515102', gosiWageSar: 5000,
    supervisesSiteIds: [], ...over };
}
 
test('scope starts at 20 beneficiaries, not 21', () => {
  const a = assess('2026-11-11', sites, []);
  assert.deepEqual(a.sitesInScope, ['jubail-a', 'jubail-b']);
  assert.equal(a.inScope, true);
});
 
test('an establishment with only small sites is out of scope', () => {
  const a = assess('2026-11-11', [{ id: 'v', beneficiaries: 19 }],
    [w('x', { saudi: false })]);
  assert.equal(a.inScope, false);
  assert.equal(a.findings.length, 0);
  assert.equal(a.compliant, true);
});
 
test('SAR 5,000 exactly counts; SAR 4,999 does not', () => {
  assert.deepEqual(checkWorker(w('ok')), []);
  assert.deepEqual(checkWorker(w('low', { gosiWageSar: 4999 })),
    [{ kind: 'wage-below-floor', workerId: 'low', shortfallSar: 1 }]);
});
 
test('a non-Saudi doing the work under another title is a breach', () => {
  const f = checkWorker(w('n', { saudi: false, occupationCode: '962101',
    supervisesSiteIds: ['jubail-a'] }));
  assert.deepEqual(f, [{ kind: 'non-saudi', workerId: 'n', role: 'actual-work' }]);
});
 
test('a Saudi doing the work under another title is a title fix, not a breach', () => {
  const a = assess('2026-11-11', sites,
    [w('s', { occupationCode: '432101', supervisesSiteIds: ['jubail-a', 'jubail-b'] })]);
  assert.equal(a.compliant, true);
  assert.deepEqual(actionPlan(a).qiwaTitleChanges, ['s']);
  assert.deepEqual(a.sitesWithoutCompliantSupervisor, []);
});
 
test('the plan adds up hires, duties and wage top-ups', () => {
  const a = assess('2026-11-11', sites, [
    w('a', { gosiWageSar: 4000, supervisesSiteIds: ['jubail-a'] }),
    w('b', { saudi: false, supervisesSiteIds: ['jubail-b'] }),
    w('c', { gosiWageSar: 4500 }),
  ]);
  assert.equal(a.compliant, false);
  assert.deepEqual(a.sitesWithoutCompliantSupervisor, ['jubail-a', 'jubail-b']);
  assert.deepEqual(actionPlan(a), { saudiHires: 1, dutiesToReassign: ['b'],
    wageTopUpSarPerMonth: 1500, qiwaTitleChanges: [] });
});
 
test('enforcement starts on 1 February 2026', () => {
  assert.equal(assess('2026-01-31', sites, []).enforced, false);
  assert.equal(assess('2026-02-01', sites, []).enforced, true);
});

شغّلها بالأمر:

npx tsx --test engine.test.ts

غيّرنا أيضًا كل مقارنة في المحرك عمدًا، مثل تحويل >= إلى > في عدد الساكنين وفي التاريخ. وكل تغيير من هذا النوع يُفشل اختبارًا واحدًا على الأقل، فالاختبارات تثبّت القواعد فعلًا.

حل المشكلات

الأداة تجد مشرفين لم تسمع بهم الموارد البشرية. هذه قاعدة العمل الفعلي تؤدي دورها. انظر من يشرف على تلك المواقع في الواقع وما مسماه في قوى. كثيرًا ما يكون رئيس عمال أو سائقًا تولى المهمة تدريجيًا.

مشرف سعودي يظهر بفرق في الأجر رغم أن راتبه يتجاوز 5,000 ريال. الأجر المسجل في التأمينات أقل من الأجر المدفوع. حدّث التسجيل. الدليل يحتسب رقم التأمينات، لا قسيمة الراتب.

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

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

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

الخلاصة

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

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