الكتابات/tutorial/2026/08
Tutorial17 أغسطس 2026·30 دقيقة

أتمتة طلبات التنفيذ القضائي السعودي عبر واجهة برمجة ناجز في TypeScript

بناء خدمة مكتوبة بـ TypeScript لتقديم طلبات التنفيذ القضائي وتتبعها عبر واجهة برمجة منصة ناجز التابعة لوزارة العدل السعودية — مع تغطية OAuth2 والتحقق من السندات التنفيذية ورفع المرافق وجدولة المطابقة اليومية.

بوابة المطورين على منصة ناجز (developers.najiz.sa) تُتيح أكثر من 160 واجهة برمجية تابعة لوزارة العدل السعودية، من بينها خدمة إنتاجية جاهزة لتقديم طلبات التنفيذ. ومع ذلك، يخلو الفضاء الرقمي من أي دليل تطبيقي بالعربية أو الإنجليزية — لا تجد سوى البوابة الحكومية ومقاطع فيديو وزارة العدل على منصة فيسبوك. هذا الدليل يملأ تلك الفجوة.

ستبني في هذا الدليل خدمة EnforcementIntakeService بـ TypeScript تنطلق من فاتورة غير مسددة وتُؤتمت دورة التنفيذ الكاملة: التحقق من السند التنفيذي، تقديم طلب التنفيذ مع المرافق، مراقبة نافذة الإشعار البالغة 5 أيام، وإبراز الطلبات التي تستوجب التصعيد. يرتبط هذا الدليل مباشرةً بمقال ناجز ونافذ: أتمتة التنفيذ السعودي على الديون الذي يشرح مسارَي التنفيذ والأخطاء الأربعة الأكثر شيوعاً. ذلك المقال ينتهي حيث يبدأ هذا الكود.

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

  • Node.js 20 أو أحدث وTypeScript 5.5 أو أحدث
  • حساب مؤسسي مفعّل على developers.najiz.sa (التقديم عبر takamul@moj.gov.sa بإرفاق السجل التجاري وحالة الاستخدام)
  • سند تنفيذي واحد على الأقل (سند إذني موثق عبر نافذ، أو حكم قضائي، أو ورقة تجارية)
  • معرفة بتدفق OAuth2 الخاص بنفاذ للتطبيقات الموجهة للأفراد — راجع دليل التكامل مع نفاذ الوطني

ما ستبنيه

src/
  types.ts          نموذج المجال وثوابت الحالة
  port.ts           واجهة NajizEnforcementPort
  client.ts         عميل HTTP لناجز (تطبيق الواجهة)
  mock-port.ts      نسخة اختبار للـCI قبل الربط الرسمي
  service.ts        خدمة استقبال الطلبات
  reconcile.ts      جدولة المطابقة اليومية
  reconcile.test.ts اختبارات vitest

نمط الـPort (نفس النهج المستخدم في دليل مطابقة مساهمات GOSI) يفصل منطق الأعمال عن وسيلة الاتصال. يمكنك بناء واختبار خط المعالجة الكامل بـMockNajizPort قبل وصول بيانات اعتماد الـAPI.

فهم واجهة برمجة التنفيذ في ناجز

تنظّم المنصة منتجاتها الـ160+ في أربعة مجالات: القضاء، التنفيذ، البورصة العقارية، والتوثيق. لأتمتة التنفيذ، نقطتا نهاية في الإنتاج هما الأساسيتان:

الاستعلام عن سندات الدائن — تُعيد جميع السندات التنفيذية المسجلة باسم الدائن. يجب استعلامها أولاً: التقديم ضد سند منتهٍ أو مستنفد هو أكثر أسباب الرفض الفوري شيوعاً.

تقديم طلب التنفيذ — يُقدّم الطلب مع بيانات المدين وتفاصيل المبلغ ومعرّفات المرافق.

بيئة الاختبار (Staging) متاحة بعد الموافقة على الحساب المؤسسي. المحاكي في الخطوة 7 يغطي فترة التطوير قبل تلك الموافقة.

إجراء التسجيل: أرسل بريداً إلى takamul@moj.gov.sa مع اسم الجهة (بالعربية والإنجليزية)، رقم السجل التجاري، حالة الاستخدام، المنتجات المطلوبة، وجهة التواصل التقني. تتضمن بيانات الاعتماد clientId وclientSecret وعنوان قاعدة API لكل بيئة.

الخطوة 1 — إعداد المشروع

npm init -y
npm install zod
npm install -D typescript @types/node tsx vitest
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "outDir": "dist"
  }
}

متغيرات البيئة:

NAJIZ_BASE_URL=https://api.najiz.sa
NAJIZ_CLIENT_ID=your-client-id
NAJIZ_CLIENT_SECRET=your-client-secret
CREDITOR_NID=1234567890

الخطوة 2 — نموذج المجال

أنواع السندات التنفيذية الثلاثة هي البنية الأساسية للنظام. الـunion التمييزي يُلزم المُصرِّف بتوفير الحقول الصحيحة في كل نقطة استخدام: السند الإذني يتطلب nafithInstrumentId وليس caseNumber. إرسال النوع الخطأ يصبح خطأ وقت الترجمة لا رفضاً في وقت التشغيل.

// src/types.ts
import { z } from 'zod'
 
// أنواع السندات التنفيذية الثلاثة
export type JudicialJudgment = {
  kind: 'judicial_judgment'
  caseNumber: string
  courtCode: string
  executionCourseDate: string  // تاريخ ISO مثل "2026-07-15"
}
 
export type NotarizedNote = {
  kind: 'notarized_note'
  nafithInstrumentId: string   // صادر من منصة نافذ للتوثيق
  notarizationDate: string
}
 
export type CommercialPaper = {
  kind: 'commercial_paper'
  paperType: 'check' | 'bill_of_exchange'
  paperNumber: string
  bankCode: string
  dueDate: string
}
 
export type InstrumentKind = JudicialJudgment | NotarizedNote | CommercialPaper
 
// هوية المدين — فرد أو كيان تجاري
export const NidSchema = z
  .string()
  .regex(/^[12]\d{9}$/, 'رقم الهوية 10 أرقام يبدأ بـ 1 (سعودي) أو 2 (إقامة)')
 
export const CrSchema = z.string().regex(/^\d{10}$/, 'رقم السجل التجاري 10 أرقام')
 
export type IndividualDebtor = { type: 'individual'; nid: string; fullName: string }
export type CommercialDebtor = {
  type: 'commercial'
  crNumber: string
  entityName: string
  representativeNid: string
}
export type Debtor = IndividualDebtor | CommercialDebtor
 
// مبلغ التنفيذ — دائماً بالريال السعودي، لا بالهللة
// (على عكس Moyasar الذي يستخدم الهللة؛ راجع دليل بوابة الدفع)
export type EnforcementAmount = {
  principalSar: number    // قيمة الدين الأصلي
  courtFeesSar: number    // رسوم المحكمة القابلة للاسترداد
  legalCostsSar: number   // أتعاب المحامي / التوثيق القابلة للاسترداد
}
 
export function totalSar(a: EnforcementAmount): number {
  return a.principalSar + a.courtFeesSar + a.legalCostsSar
}
 
// آلة حالة دورة حياة الطلب
export type RequestStatus =
  | 'submitted'           // مُقدَّم، في انتظار الإسناد للمحكمة
  | 'under_review'        // القاضي يراجع صحة السند
  | 'notice_issued'       // تم إخطار المدين؛ نافذة الرد (5 أيام) مفتوحة
  | 'grace_period'        // مهلة ممنوحة من المحكمة للمدين
  | 'enforced'            // تم تنفيذ الحكم بالكامل
  | 'partially_enforced'  // استرداد جزئي (حجز الأصول، يوجد عجز)
  | 'rejected'            // السند غير صحيح أو خلل إجرائي
  | 'suspended'           // المدين دخل إجراءات الإفلاس
  | 'withdrawn'           // الدائن سحب الطلب
 
export const TERMINAL_STATUSES = new Set<RequestStatus>([
  'enforced',
  'partially_enforced',
  'rejected',
  'suspended',
  'withdrawn',
])

الخطوة 3 — واجهة الـPort

// src/port.ts
import type { InstrumentKind, EnforcementAmount, Debtor, RequestStatus } from './types.js'
 
export type CreditorInstrument = {
  instrumentId: string
  kind: InstrumentKind
  amount: EnforcementAmount
  issuedAt: string
  status: 'valid' | 'partially_used' | 'exhausted' | 'expired'
}
 
export type EnforcementRequestInput = {
  creditorNid: string
  instrument: InstrumentKind
  debtor: Debtor
  amount: EnforcementAmount
  sourceInvoiceRef: string   // رقم الفاتورة الداخلية للتتبع
}
 
export type SubmitResult = {
  requestId: string
  referenceNumber: string   // رقم المرجع الظاهر في المراسلات
  submittedAt: string
}
 
export type StatusResult = {
  requestId: string
  status: RequestStatus
  noticeIssuedAt: string | null
  gracePeriodExpiresAt: string | null
  lastUpdatedAt: string
}
 
export interface NajizEnforcementPort {
  queryCreditorInstruments(creditorNid: string): Promise<CreditorInstrument[]>
  submitRequest(input: EnforcementRequestInput): Promise<SubmitResult>
  uploadAttachment(
    requestId: string,
    file: Buffer,
    filename: string,
  ): Promise<{ attachmentId: string }>
  getStatus(requestId: string): Promise<StatusResult>
}

الخطوة 4 — المصادقة

وصول ناجز المؤسسي يستخدم OAuth2 بأسلوب Client Credentials. يُخزّن العميل الرمز المميز مع هامش 30 ثانية لتجنب إرسال رمز منتهي الصلاحية على اتصال بطيء.

// src/client.ts
import { createHash } from 'crypto'
import { NidSchema, CrSchema } from './types.js'
import type {
  NajizEnforcementPort,
  CreditorInstrument,
  EnforcementRequestInput,
  SubmitResult,
  StatusResult,
} from './port.js'
 
export class NajizHttpClient implements NajizEnforcementPort {
  private cached: { value: string; expiresAt: number } | null = null
 
  constructor(
    private readonly baseUrl: string,
    private readonly clientId: string,
    private readonly clientSecret: string,
  ) {}
 
  private async token(): Promise<string> {
    if (this.cached && Date.now() < this.cached.expiresAt - 30_000) {
      return this.cached.value
    }
    const res = await fetch(`${this.baseUrl}/oauth2/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: this.clientId,
        client_secret: this.clientSecret,
        scope: 'enforcement:read enforcement:write',
      }),
    })
    if (!res.ok) throw new Error(`فشل في المصادقة: HTTP ${res.status}`)
    const body = (await res.json()) as { access_token: string; expires_in: number }
    this.cached = {
      value: body.access_token,
      expiresAt: Date.now() + body.expires_in * 1_000,
    }
    return this.cached.value
  }
 
  private async get<T>(path: string): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`, {
      headers: {
        Authorization: `Bearer ${await this.token()}`,
        Accept: 'application/json',
      },
    })
    if (!res.ok) throw new Error(`GET ${path} → HTTP ${res.status}`)
    return res.json() as Promise<T>
  }
 
  private async post<T>(path: string, body: unknown): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${await this.token()}`,
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify(body),
    })
    if (!res.ok) throw new Error(`POST ${path} → HTTP ${res.status}`)
    return res.json() as Promise<T>
  }

الخطوة 5 — الاستعلام عن السندات وتقديم الطلب

استعلم عن السندات دائماً قبل التقديم. سند بحالة exhausted أو expired يُنتج رفضاً فورياً — وهو أكثر أسباب الفشل في المحاولة الأولى.

  async queryCreditorInstruments(creditorNid: string): Promise<CreditorInstrument[]> {
    const body = await this.get<{ instruments: CreditorInstrument[] }>(
      `/v1/enforcement/instruments?creditorNid=${creditorNid}`,
    )
    return body.instruments
  }
 
  async submitRequest(input: EnforcementRequestInput): Promise<SubmitResult> {
    // التحقق من هوية المدين قبل الإرسال عبر الشبكة
    if (input.debtor.type === 'individual') {
      NidSchema.parse(input.debtor.nid)
    } else {
      CrSchema.parse(input.debtor.crNumber)
      NidSchema.parse(input.debtor.representativeNid)
    }
    return this.post<SubmitResult>('/v1/enforcement/requests', buildPayload(input))
  }

buildPayload تُحوّل الـunion التمييزي إلى كائن API مسطّح. عبارة switch بلا default مقصودة: فحص الاستنفاد في TypeScript يُنبّه إذا أُضيف نوع سند جديد للـunion ولم يُعالَج.

function buildPayload(input: EnforcementRequestInput) {
  const base = {
    creditorNid: input.creditorNid,
    sourceInvoiceRef: input.sourceInvoiceRef,
    amount: {
      principal: input.amount.principalSar,
      courtFees: input.amount.courtFeesSar,
      legalCosts: input.amount.legalCostsSar,
    },
    debtor:
      input.debtor.type === 'individual'
        ? { type: 'individual', nid: input.debtor.nid, name: input.debtor.fullName }
        : {
            type: 'commercial',
            crNumber: input.debtor.crNumber,
            name: input.debtor.entityName,
            representativeNid: input.debtor.representativeNid,
          },
  }
 
  switch (input.instrument.kind) {
    case 'judicial_judgment':
      return {
        ...base,
        instrumentType: 'judicial_judgment',
        caseNumber: input.instrument.caseNumber,
        courtCode: input.instrument.courtCode,
        executionCourseDate: input.instrument.executionCourseDate,
      }
    case 'notarized_note':
      return {
        ...base,
        instrumentType: 'notarized_note',
        nafithInstrumentId: input.instrument.nafithInstrumentId,
        notarizationDate: input.instrument.notarizationDate,
      }
    case 'commercial_paper':
      return {
        ...base,
        instrumentType: 'commercial_paper',
        paperType: input.instrument.paperType,
        paperNumber: input.instrument.paperNumber,
        bankCode: input.instrument.bankCode,
        dueDate: input.instrument.dueDate,
      }
  }
}

الخطوة 6 — رفع المرافق

الوثائق الداعمة (السند الموقّع، نسخ الفاتورة، وكالة قانونية) تُرفع منفصلةً بعد إنشاء الطلب. يحمي تجزئة SHA-256 من الفساد الصامت عند الرفع.

أسماء الملفات بالعربية (مثل سند-إذني-١٤٤٨.pdf) تُمرَّر كمعامل ثالث لـFormData.append. لا تشفّرها بترميز URL قبل الإرسال — FormData تتولى الترميز داخلياً.

  async uploadAttachment(
    requestId: string,
    file: Buffer,
    filename: string,
  ): Promise<{ attachmentId: string }> {
    const form = new FormData()
    form.append('file', new Blob([file], { type: 'application/pdf' }), filename)
    form.append('requestId', requestId)
    form.append('contentHash', createHash('sha256').update(file).digest('hex'))
 
    const res = await fetch(`${this.baseUrl}/v1/enforcement/attachments`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${await this.token()}` },
      body: form,
    })
    if (!res.ok) throw new Error(`فشل رفع المرفق: HTTP ${res.status}`)
    return res.json() as Promise<{ attachmentId: string }>
  }
 
  async getStatus(requestId: string): Promise<StatusResult> {
    return this.get<StatusResult>(`/v1/enforcement/requests/${requestId}/status`)
  }
}

الخطوة 7 — جدولة المطابقة اليومية

نافذة الإشعار البالغة 5 أيام تعمل بصمت. تُصدر محكمة التنفيذ إشعاراً للمدين ولا يصلك أي webhook. تحتاج إلى مهمة يومية تُصنّف الطلبات غير المنتهية حسب حالة التصعيد.

لماذا asOf لا new Date(): مهمة تعمل الساعة 3 صباحاً يجب أن تُنتج نفس النتائج عند إعادة التشغيل الساعة 4 صباحاً إن لم يتغير شيء في ناجز. تمرير asOf صراحةً يجعل الدالة محددة وقابلة للاختبار بتواريخ ثابتة.

// src/reconcile.ts
import { TERMINAL_STATUSES, type RequestStatus } from './types.js'
import type { NajizEnforcementPort, StatusResult } from './port.js'
 
export type OutstandingRow = {
  requestId: string
  status: RequestStatus
  noticeIssuedAt: string | null
}
 
export type ReconciliationResult = {
  requestId: string
  prevStatus: RequestStatus
  newStatus: RequestStatus
  daysSinceNotice: number | null
  needsEscalation: boolean       // notice_issued وتجاوز 5 أيام
  gracePeriodExpired: boolean    // grace_period وانتهت المهلة
}
 
export async function reconcileRequests(
  port: NajizEnforcementPort,
  rows: OutstandingRow[],
  asOf: Date,
): Promise<ReconciliationResult[]> {
  const pending = rows.filter((r) => !TERMINAL_STATUSES.has(r.status))
 
  return Promise.all(
    pending.map(async (row): Promise<ReconciliationResult> => {
      const current = await port.getStatus(row.requestId)
 
      const daysSinceNotice =
        row.noticeIssuedAt != null
          ? Math.floor(
              (asOf.getTime() - new Date(row.noticeIssuedAt).getTime()) / 86_400_000,
            )
          : null
 
      return {
        requestId: row.requestId,
        prevStatus: row.status,
        newStatus: current.status,
        daysSinceNotice,
        needsEscalation:
          current.status === 'notice_issued' && (daysSinceNotice ?? 0) > 5,
        gracePeriodExpired:
          current.status === 'grace_period' &&
          current.gracePeriodExpiresAt != null &&
          new Date(current.gracePeriodExpiresAt) < asOf,
      }
    }),
  )
}

الخطوة 8 — محاكي الـPort للاختبار

لا توجد بيئة sandbox عامة، لذا يُتيح محاكي الـPort تشغيل خط المعالجة الكامل في CI. المساعد advance يُحرّك الطلب إلى أي حالة دون الاتصال بالشبكة.

// src/mock-port.ts
import { createHash } from 'crypto'
import { TERMINAL_STATUSES, type RequestStatus } from './types.js'
import type {
  NajizEnforcementPort,
  CreditorInstrument,
  EnforcementRequestInput,
  SubmitResult,
  StatusResult,
} from './port.js'
 
export class MockNajizPort implements NajizEnforcementPort {
  private readonly store = new Map<string, StatusResult>()
  private seq = 0
 
  async queryCreditorInstruments(
    _creditorNid: string,
  ): Promise<CreditorInstrument[]> {
    return []  // اسكن البيانات التجريبية في كل اختبار
  }
 
  async submitRequest(_input: EnforcementRequestInput): Promise<SubmitResult> {
    const id = `MOCK-${String(++this.seq).padStart(6, '0')}`
    this.store.set(id, {
      requestId: id,
      status: 'submitted',
      noticeIssuedAt: null,
      gracePeriodExpiresAt: null,
      lastUpdatedAt: new Date().toISOString(),
    })
    return {
      requestId: id,
      referenceNumber: `REF-${id}`,
      submittedAt: new Date().toISOString(),
    }
  }
 
  async uploadAttachment(
    _requestId: string,
    file: Buffer,
    _filename: string,
  ): Promise<{ attachmentId: string }> {
    return {
      attachmentId: `ATT-${createHash('sha256').update(file).digest('hex').slice(0, 12)}`,
    }
  }
 
  async getStatus(requestId: string): Promise<StatusResult> {
    const r = this.store.get(requestId)
    if (!r) throw new Error(`MockNajizPort: requestId غير معروف ${requestId}`)
    return r
  }
 
  /** مساعد الاختبار: نقل الطلب إلى حالة معينة */
  advance(
    requestId: string,
    status: RequestStatus,
    patch: Partial<
      Pick<StatusResult, 'noticeIssuedAt' | 'gracePeriodExpiresAt'>
    > = {},
  ): void {
    const prev = this.store.get(requestId)
    if (!prev) throw new Error(`MockNajizPort: requestId غير معروف ${requestId}`)
    this.store.set(requestId, {
      ...prev,
      status,
      lastUpdatedAt: new Date().toISOString(),
      ...patch,
    })
  }
}

الاختبارات

// src/reconcile.test.ts
import { describe, it, expect } from 'vitest'
import { MockNajizPort } from './mock-port.js'
import { reconcileRequests } from './reconcile.js'
 
const SAMPLE_INPUT: import('./port.js').EnforcementRequestInput = {
  creditorNid: '1234567890',
  instrument: {
    kind: 'notarized_note',
    nafithInstrumentId: 'NF-2026-001',
    notarizationDate: '2026-08-01',
  },
  debtor: { type: 'individual', nid: '2345678901', fullName: 'اسم المدين' },
  amount: { principalSar: 50_000, courtFeesSar: 1_000, legalCostsSar: 500 },
  sourceInvoiceRef: 'INV-2026-0042',
}
 
describe('reconcileRequests', () => {
  it('يكشف notice_issued المتجاوز للـ5 أيام', async () => {
    const port = new MockNajizPort()
    const { requestId } = await port.submitRequest(SAMPLE_INPUT)
 
    const noticeIssuedAt = '2026-08-10T08:00:00.000Z'
    port.advance(requestId, 'notice_issued', { noticeIssuedAt })
 
    const results = await reconcileRequests(
      port,
      [{ requestId, status: 'notice_issued', noticeIssuedAt }],
      new Date('2026-08-16T08:00:00.000Z'),  // 6 أيام لاحقاً
    )
 
    expect(results[0]?.needsEscalation).toBe(true)
    expect(results[0]?.daysSinceNotice).toBe(6)
  })
 
  it('يتجاهل الطلبات في الحالات النهائية', async () => {
    const port = new MockNajizPort()
    const { requestId } = await port.submitRequest(SAMPLE_INPUT)
    port.advance(requestId, 'enforced')
 
    const results = await reconcileRequests(
      port,
      [{ requestId, status: 'enforced', noticeIssuedAt: null }],
      new Date('2026-08-17T00:00:00.000Z'),
    )
 
    expect(results).toHaveLength(0)
  })
 
  it('يكشف انتهاء مهلة المدين', async () => {
    const port = new MockNajizPort()
    const { requestId } = await port.submitRequest(SAMPLE_INPUT)
 
    port.advance(requestId, 'grace_period', {
      gracePeriodExpiresAt: '2026-08-14T00:00:00.000Z',
    })
 
    const results = await reconcileRequests(
      port,
      [{ requestId, status: 'grace_period', noticeIssuedAt: '2026-08-10T08:00:00.000Z' }],
      new Date('2026-08-15T00:00:00.000Z'),
    )
 
    expect(results[0]?.gracePeriodExpired).toBe(true)
  })
})

ملاحظة صادقة: الوصول للـAPI يتطلب تسجيلاً مؤسسياً

بوابة مطورين ناجز لا تُتيح بيانات اعتماد sandbox عامة. بيئة الاختبار موجودة لكنها تتطلب تسجيلاً مؤسسياً عبر takamul@moj.gov.sa. قدّم اسم جهتك، رقم السجل التجاري، حالة الاستخدام، المنتجات المطلوبة، وجهة التواصل التقني. هذا يعكس النموذج المتبع في GOSI وWPS وسابر — التنفيذ القضائي بنية تحتية حساسة ذات سيادة، والمفاتيح المفتوحة ليست نموذج النشر للخدمات الحكومية السعودية.

المسار العملي: ابنِ واختبر بـMockNajizPort. حين تصل بيانات الاعتماد، استبدل التطبيق — منطق الأعمال لا يتغير.

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

رفض فوري عند التقديم — في الغالب مشكلة أهلية السند. حقل status في نتيجة queryCreditorInstruments يجب أن يكون valid؛ السند exhausted أو expired ليس خطأ في كودك بل في دورة حياة السند. جدّده عبر منصة نافذ أو احصل على حكم جديد.

عدم تطابق نوع المدين — المدين الفرد يتطلب هوية مطابقة للنمط /^[12]\d{9}$/؛ الكيان التجاري يتطلب crNumber من 10 أرقام وهوية الممثل. الخلط بينهما يُنتج رفضاً من المحكمة لا خطأ شبكياً.

مرافق مكررة عند إعادة المحاولة — كل استدعاء رفع يُنشئ attachmentId جديداً. إذا انتهت مهلة الرفع الأول، تحقق من نجاحه قبل إعادة المحاولة. المرافق المكررة لا تُرفض لكنها تزيد حجم الملف في المحكمة دون فائدة.

إعادة التصعيد أثناء مهلة المدين — مدين في حالة grace_period يعني أن المحكمة منحته مهلة. لا تُصعّد حتى يمر gracePeriodExpiresAt. التنبيه المبكر يُفقد فريق العمليات ثقته بنظام التنبيه خلال أسبوع.

الخلاصة

بنيت الآن خدمة استقبال طلبات تنفيذ مكتوبة بـ TypeScript تتحقق من السندات، تُقدّم الطلبات، ترفع المرافق، وتشغّل مطابقة يومية على نافذة الإشعار البالغة 5 أيام — كل ذلك خلف واجهة Port تعمل في CI اليوم.

للخطوة الأعلى — توليد السند التنفيذي الذي تقبله محكمة التنفيذ — راجع المقال المرافق ناجز ونافذ: أتمتة التنفيذ السعودي على الديون.

للفاتورة التي أصبحت ذمة مالية تُنفّذها الآن، يغطي دليل التكامل مع ZATCA المرحلة الثانية التخليص الإلكتروني — الوثيقة التي تطلبها المحكمة دليلاً على الالتزام الأساسي.

نمط المطابقة هنا — واجهة Port، تصفية الحالات النهائية، معامل asOf — يظهر أيضاً في دليل مطابقة مساهمات GOSI وينقل مباشرةً عبر مجالات الامتثال المالي.

إذا كانت أعمالك تدير ذمماً مدينة في السعودية وتحتاج إلى طبقة أتمتة بين نظام ERP وبوابة التنفيذ، تواصل مع نقطة. نتخصص في طبقة التكامل فوق الأنظمة القائمة — ربط ما يُنتجه الـERP بما تتطلبه البوابة الحكومية، دون بناء أيٍّ منهما من الصفر.