Qoyod
الأسعار

 دليل المعرفة

أمثلة تكامل بلغة Node.js

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

الأمثلة هنا تستخدم مكتبات قياسية في بيئة Node.js: الوحدة المدمجة crypto لحساب قيمة التجزئة (Hash)، والوحدة Buffer لترميز Base64، ومكتبة axios أو الدالة المدمجة fetch لإرسال الطلبات. كل مقطع برمجي مكتوب بأسلوب async/await مع معالجة أخطاء حقيقية وآلية إعادة محاولة، لأن التكامل مع أي واجهة برمجة تطبيقات حكومية يتطلب صلابة وليس مجرد طلب ناجح في الظروف المثالية.

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

لماذا Node.js مناسبة لتكامل الفوترة الإلكترونية

تتميز بيئة Node.js بطبيعتها غير المتزامنة (Asynchronous)، وهذا يجعلها خيارًا منطقيًا لأي تكامل يعتمد على طلبات شبكة متتالية. عند إرسال فاتورة إلى منصة فاتورة، أنت تنتظر ردًا من خادم بعيد، وخلال هذا الانتظار لا ترغب في تجميد التطبيق بأكمله. نموذج الحلقة الواحدة (Event Loop) في Node.js يسمح لك بإطلاق عشرات الطلبات بالتوازي دون استهلاك خيوط معالجة إضافية.

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

النقطة الأهم: التكامل الصحيح ليس عن «إرسال طلب واحد ناجح». التكامل الصحيح يعني التعامل مع انقطاع الشبكة، ومع ردود الخطأ من المنصة، ومع المهل الزمنية (Timeouts)، ومع حالات تتطلب إعادة المحاولة. الكود الذي يعمل في بيئة التطوير ثم ينهار في الإنتاج هو الكود الذي تجاهل هذه الحالات. لذلك ستجد في كل مثال معالجة أخطاء واضحة.

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

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

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

مسار التكامل بلغة Node.js
الخطوات التي ينفّذها كود Node.js لإرسال فاتورة متوافقة.
1

بناء ملف UBL والتوقيع

2

حساب التجزئة SHA-256

3

ترميز Base64 وبناء ترويسة Basic

4

POST للمقاصة أو الإبلاغ

5

قراءة الاستجابة وحفظ النتيجة

يغلّف كود Node.js هذه الخطوات في دوال قابلة لإعادة الاستخدام.

الخطوة الأولى: بناء ترويسة المصادقة (Basic Auth)

كل طلب يُرسَل إلى منصة فاتورة يجب أن يحمل ترويسة تفويض (Authorization Header). تستخدم المنصة أسلوب المصادقة الأساسي (Basic Authentication)، وهو ترميز Base64 لمعرّف الختم المشفّر للامتثال (CSID) مع كلمة السر المرتبطة به، مفصولين بنقطتين رأسيتين.

انتبه إلى المصطلح بدقة: الاختصار الصحيح هو CSID، أي معرّف الختم المشفّر للامتثال (Compliance Cryptographic Stamp Identifier). هذا المعرّف تحصل عليه من هيئة الزكاة والضريبة والجمارك عند تسجيل وحدة الإصدار الخاصة بك. لا تخلط بينه وبين أي اختصار آخر.

في Node.js، نبني هذه الترويسة باستخدام الوحدة المدمجة Buffer. ننشئ سلسلة نصية تجمع المعرّف وكلمة السر، ثم نُرمّزها إلى Base64. الكود التالي يوضّح ذلك:

// auth.js — بناء ترويسة المصادقة الأساسية
'use strict';

/**
 * يبني ترويسة Authorization بصيغة Basic
 * @param {string} csid - معرّف الختم المشفّر للامتثال (Compliance CSID)
 * @param {string} secret - كلمة السر المرتبطة بالـ CSID
 * @returns {string} قيمة ترويسة Authorization جاهزة
 */
function buildAuthHeader(csid, secret) {
  if (!csid || !secret) {
    throw new Error('CSID and secret are required to build the auth header');
  }
  const credentials = `${csid}:${secret}`;
  const encoded = Buffer.from(credentials, 'utf8').toString('base64');
  return `Basic ${encoded}`;
}

module.exports = { buildAuthHeader };

لاحظ أننا نُمرّر 'utf8' صراحةً كترميز للمدخلات، وهذا مهم لأن المعرّف قد يحتوي على رموز خاصة. تمرير الترميز بوضوح يمنع سلوكًا غير متوقع عبر إصدارات Node.js المختلفة. كما نتحقق من وجود القيمتين قبل البناء، لأن ترويسة مصادقة ناقصة ستُرفض من المنصة برسالة غير واضحة، والتحقق المبكر يوفّر عليك تتبّع المشكلة لاحقًا.

القاعدة الذهبية هنا: لا تُخزّن CSID ولا كلمة السر داخل الكود مباشرةً. اقرأهما من متغيرات البيئة (Environment Variables) أو من خدمة إدارة أسرار. أي بيانات اعتماد مكتوبة في الكود المصدري هي ثغرة أمنية، خصوصًا إذا رُفع المشروع إلى مستودع مشترك.

// قراءة بيانات الاعتماد من متغيرات البيئة (الطريقة الآمنة)
const csid = process.env.QOYOD_CSID;
const secret = process.env.QOYOD_SECRET;

const authHeader = buildAuthHeader(csid, secret);
// النتيجة: "Basic ZXhhbXBsZUNTSUQ6c2VjcmV0..."

الخطوة الثانية: حساب قيمة تجزئة الفاتورة (SHA-256 invoiceHash)

تتطلب الفوترة الإلكترونية في المرحلة الثانية حساب قيمة تجزئة (Hash) لكل فاتورة باستخدام خوارزمية SHA-256. هذه القيمة تضمن سلامة المحتوى وتُستخدم في سلسلة ربط الفواتير المتتالية، حيث تحمل كل فاتورة تجزئة الفاتورة السابقة لها لضمان عدم التلاعب.

الوحدة المدمجة crypto في Node.js تتولى هذا الحساب بسطر واحد فعليًا. نُمرّر محتوى الفاتورة بصيغة UBL XML إلى دالة التجزئة، ونستخرج النتيجة بترميز Base64 كما تتطلب مواصفات المنصة:

// hash.js — حساب قيمة تجزئة الفاتورة
'use strict';
const crypto = require('crypto');

/**
 * يحسب SHA-256 لمحتوى الفاتورة ويُرجعه بصيغة Base64
 * @param {string} ublXml - محتوى الفاتورة بصيغة UBL 2.1 XML
 * @returns {string} قيمة التجزئة بترميز Base64
 */
function computeInvoiceHash(ublXml) {
  if (typeof ublXml !== 'string' || ublXml.length === 0) {
    throw new Error('ublXml must be a non-empty string');
  }
  return crypto
    .createHash('sha256')
    .update(ublXml, 'utf8')
    .digest('base64');
}

module.exports = { computeInvoiceHash };

الدالة createHash('sha256') تُنشئ كائن تجزئة، ثم update() يُغذّيه بالمحتوى، وأخيرًا digest('base64') يُنهي الحساب ويُرجع النتيجة جاهزة. بعض التكاملات تحتاج التجزئة بصيغة سداسية عشرية (Hex) بدلًا من Base64، وفي تلك الحالة تستبدل 'base64' بـ 'hex'. تحقّق دائمًا من الصيغة المطلوبة في توثيق نقطة النهاية التي تتعامل معها.

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

سلسلة التجزئة في الكود
كيف يربط الكود كل فاتورة بتجزئة سابقتها.
1

فاتورة 1 + تجزئتها

2

فاتورة 2 تحمل تجزئة 1

3

فاتورة 3 تحمل تجزئة 2

يحفظ الكود تجزئة كل فاتورة لتغذية الفاتورة التالية.

الخطوة الثالثة: ترميز محتوى UBL إلى Base64

قبل إرسال الفاتورة، يجب ترميز محتوى UBL XML بالكامل إلى Base64. منصة فاتورة تستقبل الفاتورة كنص مُرمّز ضمن جسم الطلب بصيغة JSON، وليس كملف XML خام. هذا الترميز يضمن نقل المحتوى عبر HTTP دون مشاكل في الرموز الخاصة.

كما في حساب التجزئة، نستخدم الوحدة Buffer. الترميز هنا عملية واحدة مباشرة:

// encode.js — ترميز محتوى UBL إلى Base64
'use strict';

/**
 * يُرمّز محتوى UBL XML إلى Base64
 * @param {string} ublXml - محتوى الفاتورة بصيغة UBL 2.1 XML
 * @returns {string} المحتوى مُرمّزًا بـ Base64
 */
function encodeInvoice(ublXml) {
  return Buffer.from(ublXml, 'utf8').toString('base64');
}

/**
 * يفكّ ترميز محتوى Base64 إلى نص (مفيد للتحقق والتصحيح)
 * @param {string} encoded - المحتوى المُرمّز
 * @returns {string} المحتوى الأصلي
 */
function decodeInvoice(encoded) {
  return Buffer.from(encoded, 'base64').toString('utf8');
}

module.exports = { encodeInvoice, decodeInvoice };

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

تذكّر أن التجزئة من الخطوة الثانية تُحسب على محتوى XML الأصلي قبل الترميز، وليس على النسخة المُرمّزة بـ Base64. هذا فرق جوهري: التجزئة على المحتوى الخام، والإرسال للمحتوى المُرمّز. الخلط بين الاثنين يُنتج رفضًا من المنصة.

الخطوة الرابعة: إرسال الطلب إلى مسار المعالجة (Clearance) أو الإبلاغ (Reporting)

الآن نصل إلى قلب التكامل: إرسال الفاتورة فعليًا. هنا يجب أن تفهم تمييزًا أساسيًا فرضته هيئة الزكاة والضريبة والجمارك بين نوعين من الفواتير:

  • الفاتورة الضريبية (B2B): تُرسَل إلى مسار المعالجة (Clearance) ويجب أن تُعتمد من المنصة فوريًا قبل تسليمها للمشتري.
  • الفاتورة الضريبية المبسّطة (B2C): تُرسَل إلى مسار الإبلاغ (Reporting) خلال 24 ساعة من إصدارها، وتُسلَّم للمشتري مباشرة.

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

// submit-axios.js — إرسال الفاتورة باستخدام axios
'use strict';
const axios = require('axios');
const { buildAuthHeader } = require('./auth');
const { computeInvoiceHash } = require('./hash');
const { encodeInvoice } = require('./encode');

const BASE_URL = process.env.QOYOD_API_BASE || 'https://api.qoyod.com/einvoicing';

/**
 * يُرسل فاتورة إلى مسار المعالجة (B2B) أو الإبلاغ (B2C)
 * @param {Object} options
 * @param {string} options.ublXml - محتوى الفاتورة UBL XML
 * @param {string} options.uuid - المعرّف الفريد للفاتورة
 * @param {('clearance'|'reporting')} options.mode - نوع المسار
 * @param {string} options.csid - معرّف الختم المشفّر للامتثال
 * @param {string} options.secret - كلمة السر
 * @returns {Promise<Object>} رد المنصة بصيغة JSON
 */
async function submitInvoice({ ublXml, uuid, mode, csid, secret }) {
  const invoiceHash = computeInvoiceHash(ublXml);
  const invoiceBase64 = encodeInvoice(ublXml);
  const endpoint = mode === 'clearance'
    ? `${BASE_URL}/invoices/clearance/single`
    : `${BASE_URL}/invoices/reporting/single`;

  const response = await axios.post(
    endpoint,
    {
      invoiceHash,
      uuid,
      invoice: invoiceBase64,
    },
    {
      headers: {
        'Authorization': buildAuthHeader(csid, secret),
        'Content-Type': 'application/json',
        'Accept-Language': 'ar',
      },
      timeout: 30000, // مهلة 30 ثانية
    }
  );

  return response.data;
}

module.exports = { submitInvoice };

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

إذا كنت تفضّل عدم إضافة مكتبة خارجية، فإن الدالة المدمجة fetch المتوفرة في Node.js منذ الإصدار 18 تؤدي الغرض نفسه. إليك النسخة المكافئة:

// submit-fetch.js — إرسال الفاتورة باستخدام fetch المدمجة
'use strict';
const { buildAuthHeader } = require('./auth');
const { computeInvoiceHash } = require('./hash');
const { encodeInvoice } = require('./encode');

const BASE_URL = process.env.QOYOD_API_BASE || 'https://api.qoyod.com/einvoicing';

async function submitInvoiceFetch({ ublXml, uuid, mode, csid, secret }) {
  const invoiceHash = computeInvoiceHash(ublXml);
  const invoiceBase64 = encodeInvoice(ublXml);
  const endpoint = mode === 'clearance'
    ? `${BASE_URL}/invoices/clearance/single`
    : `${BASE_URL}/invoices/reporting/single`;

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 30000);

  try {
    const res = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Authorization': buildAuthHeader(csid, secret),
        'Content-Type': 'application/json',
        'Accept-Language': 'ar',
      },
      body: JSON.stringify({ invoiceHash, uuid, invoice: invoiceBase64 }),
      signal: controller.signal,
    });

    if (!res.ok) {
      const errorBody = await res.text();
      throw new Error(`HTTP ${res.status}: ${errorBody}`);
    }
    return await res.json();
  } finally {
    clearTimeout(timer);
  }
}

module.exports = { submitInvoiceFetch };

الفرق الجوهري أن fetch لا يدعم المهلة الزمنية مباشرة، لذلك استخدمنا AbortController مع مؤقّت لإلغاء الطلب بعد 30 ثانية. كذلك، على عكس axios، لا ترمي fetch خطأً عند ردود الفشل مثل 400 أو 500، لذلك نتحقق يدويًا من res.ok ونرمي الخطأ بأنفسنا. هذا الاختلاف يُربك كثيرًا من المطوّرين القادمين من مكتبات أخرى.

الخطوة الخامسة: تحليل رد المنصة ومعالجة الأخطاء

الطلب الناجح ليس نهاية القصة. المنصة تُرجع ردًا بصيغة JSON يحتوي على حالة الفاتورة، وقد تكون «مقبولة» أو «مقبولة مع ملاحظات» أو «مرفوضة». يجب أن يتعامل كودك مع الحالات الثلاث، لا أن يفترض النجاح دائمًا.

// handle-response.js — تحليل رد المنصة
'use strict';

/**
 * يحلّل رد المنصة ويُصنّف نتيجة الفاتورة
 * @param {Object} response - رد المنصة بصيغة JSON
 * @returns {Object} نتيجة مُصنّفة
 */
function parseClearanceResult(response) {
  const status = response.clearanceStatus || response.reportingStatus;

  switch (status) {
    case 'CLEARED':
    case 'REPORTED':
      return {
        ok: true,
        status,
        clearedInvoice: response.clearedInvoice, // الفاتورة الموقّعة من المنصة
        warnings: response.warningMessages || [],
      };

    case 'CLEARED_WITH_WARNINGS':
    case 'REPORTED_WITH_WARNINGS':
      return {
        ok: true,
        status,
        clearedInvoice: response.clearedInvoice,
        warnings: response.warningMessages || [],
      };

    default:
      return {
        ok: false,
        status: status || 'UNKNOWN',
        errors: response.errorMessages || [{ message: 'استجابة غير متوقعة من المنصة' }],
      };
  }
}

module.exports = { parseClearanceResult };

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

أما معالجة الأخطاء على مستوى الشبكة فهي قصة أخرى. عند فشل الطلب بسبب انقطاع مؤقت أو خطأ خادم 502 أو انتهاء المهلة، الحل الصحيح هو إعادة المحاولة بأسلوب التراجع الأسّي (Exponential Backoff)، لا إعادة المحاولة فورًا. الكود التالي يلفّ دالة الإرسال بآلية إعادة محاولة ذكية:

// retry.js — إعادة المحاولة مع تراجع أسّي
'use strict';

/**
 * ينفّذ دالة غير متزامنة مع إعادة المحاولة عند الفشل المؤقت
 * @param {Function} fn - الدالة المراد تنفيذها
 * @param {Object} opts
 * @param {number} opts.retries - عدد المحاولات (الافتراضي 3)
 * @param {number} opts.baseDelay - التأخير الأساسي بالميلي ثانية
 */
async function withRetry(fn, { retries = 3, baseDelay = 1000 } = {}) {
  let lastError;

  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err;
      const status = err.response && err.response.status;
      const retriable =
        !status || status === 429 || status === 502 ||
        status === 503 || status === 504 || err.code === 'ECONNABORTED';

      if (!retriable || attempt === retries) {
        break;
      }
      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.warn(`المحاولة ${attempt} فشلت، إعادة بعد ${delay}ms`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
  throw lastError;
}

module.exports = { withRetry };

هذه الدالة تميّز بين الأخطاء القابلة لإعادة المحاولة والأخطاء النهائية. خطأ مثل 400 (طلب غير صالح) أو 401 (مصادقة فاشلة) لا فائدة من إعادة محاولته، لأن المشكلة في طلبك لا في الخادم. أما 429 (تجاوز الحد) أو 502 و503 و504 أو انتهاء المهلة فهي مؤقتة وتستحق إعادة المحاولة. التأخير يتضاعف مع كل محاولة: ثانية، ثم ثانيتان، ثم أربع ثوان، ما يمنح الخادم فرصة للتعافي دون إغراقه بطلبات متلاحقة.

لماذا التراجع الأسّي تحديدًا وليس تأخيرًا ثابتًا؟ تخيّل أن المنصة تعاني ضغطًا مؤقتًا، وأن مئات التطبيقات تُعيد المحاولة في اللحظة نفسها. لو أعاد الجميع المحاولة بعد ثانية ثابتة، لتجمّعت الطلبات في موجات متزامنة تزيد الضغط. التراجع الأسّي يوزّع المحاولات على فترات متباعدة، ويمنح النظام البعيد مجالًا للتعافي. بعض التطبيقات المتقدّمة تضيف عنصرًا عشوائيًا صغيرًا إلى التأخير لتفادي التزامن تمامًا، وهو أسلوب يُعرف بالتشويش (Jitter).

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

تجميع الرحلة الكاملة في دالة واحدة

الآن نجمع كل القطع السابقة في تدفّق واحد متكامل يأخذ فاتورة من بدايتها حتى استلام النتيجة النهائية. هذا هو الكود الذي ستستدعيه فعليًا من نظامك:

// index.js — الرحلة الكاملة
'use strict';
const { submitInvoice } = require('./submit-axios');
const { parseClearanceResult } = require('./handle-response');
const { withRetry } = require('./retry');

/**
 * يُرسل فاتورة ويُعيد النتيجة المُصنّفة مع إعادة محاولة عند الفشل المؤقت
 */
async function processInvoice({ ublXml, uuid, isB2B }) {
  const csid = process.env.QOYOD_CSID;
  const secret = process.env.QOYOD_SECRET;
  const mode = isB2B ? 'clearance' : 'reporting';

  try {
    const raw = await withRetry(
      () => submitInvoice({ ublXml, uuid, mode, csid, secret }),
      { retries: 3, baseDelay: 1000 }
    );

    const result = parseClearanceResult(raw);

    if (!result.ok) {
      console.error('رُفضت الفاتورة:', result.errors);
      return result;
    }

    if (result.warnings.length > 0) {
      console.warn('قُبلت مع ملاحظات:', result.warnings);
    }

    console.log(`تمت معالجة الفاتورة ${uuid} بحالة ${result.status}`);
    return result;
  } catch (err) {
    console.error(`فشل نهائي في معالجة الفاتورة ${uuid}:`, err.message);
    throw err;
  }
}

module.exports = { processInvoice };

// مثال استخدام:
// const { processInvoice } = require('./index');
// await processInvoice({ ublXml: myUbl, uuid: myUuid, isB2B: true });

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

ابدأ اليوم

تكامل فوترة إلكترونية جاهز دون أن تكتب سطرًا

يتولّى قيود توقيع الفواتير وختمها وربطها بمنصة فاتورة تلقائيًا، بمعالجة فورية للفواتير الضريبية وإبلاغ خلال 24 ساعة للمبسّطة. جرّب المنصة وركّز على عملك بدل بناء التكامل من الصفر.

ابدأ تجربتك المجانية وفعّل الفوترة الإلكترونية

تنظيم المشروع وإدارة الإعدادات

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

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

// config.js — إعدادات مركزية تقرأ من البيئة
'use strict';

const config = {
  apiBase: process.env.QOYOD_API_BASE || 'https://api.qoyod.com/einvoicing',
  csid: process.env.QOYOD_CSID,
  secret: process.env.QOYOD_SECRET,
  timeout: parseInt(process.env.QOYOD_TIMEOUT || '30000', 10),
  retries: parseInt(process.env.QOYOD_RETRIES || '3', 10),
  batchSize: parseInt(process.env.QOYOD_BATCH_SIZE || '5', 10),
};

// تحقق مبكر من الإعدادات الإلزامية عند الإقلاع
function validateConfig() {
  const required = ['csid', 'secret'];
  const missing = required.filter((key) => !config[key]);
  if (missing.length > 0) {
    throw new Error(`إعدادات ناقصة: ${missing.join(', ')}`);
  }
}

module.exports = { config, validateConfig };

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

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

اختبار التكامل قبل الإنتاج

لا تُجرّب تكاملك لأول مرة على فواتير حقيقية. منصة فاتورة توفّر بيئة اختبار (Sandbox) منفصلة تسمح لك بإرسال فواتير تجريبية والحصول على ردود واقعية دون أثر ضريبي فعلي. وجّه عنوان المنصة الأساسي في إعداداتك إلى بيئة الاختبار أثناء التطوير، ثم بدّله إلى الإنتاج فقط بعد أن يجتاز كودك كل السيناريوهات.

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

كتابة اختبارات آلية لهذه السيناريوهات استثمار يؤتي ثماره سريعًا. باستخدام أي إطار اختبار في Node.js، يمكنك محاكاة ردود المنصة المختلفة والتأكد من أن دوال التحليل وإعادة المحاولة تتصرّف كما هو متوقع. الاختبار الآلي يحميك من أن يكسر تعديل لاحق منطقًا كان يعمل، وهو أمر يحدث كثيرًا في الأنظمة طويلة العمر.

المقاصة مقابل الإبلاغ في الكود
كيف يختار الكود النقطة الصحيحة حسب نوع الفاتورة.
المعيار Clearance (B2B) Reporting (B2C)
النقطة clearance/single reporting/single
Clearance-Status 1 0
التوقيت قبل التسليم خلال 24 ساعة
يبدّل الكود النقطة وقيمة Clearance-Status حسب نوع الفاتورة.

أخطاء شائعة في تكامل Node.js وكيف تتجنّبها

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

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

الخطأ الثالث هو الاعتماد على نجاح الطلب دون فحص محتوى الرد. الطلب قد يعود بحالة 200 لكن الفاتورة مرفوضة داخل جسم الرد. افحص دائمًا حقل الحالة في JSON، لا حالة HTTP وحدها.

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

الخطأ الخامس والأخطر أمنيًا هو كتابة بيانات الاعتماد في الكود أو في سجلّات التطبيق. لا تطبع CSID ولا كلمة السر في أي console.log، ولا تُخزّنهما في الكود المصدري. استخدم متغيرات البيئة أو خدمة أسرار مخصّصة.

التعامل مع الفواتير بالجملة (Batch)

عندما تحتاج إرسال عدد كبير من الفواتير، الطبيعة غير المتزامنة لـ Node.js تتألّق. لكن لا ترسلها كلها دفعة واحدة، لأن ذلك قد يتجاوز حدود المعدّل (Rate Limits) في المنصة ويُسبّب أخطاء 429. الحل هو معالجتها على دفعات محدودة الحجم:

// batch.js — معالجة الفواتير على دفعات
'use strict';
const { processInvoice } = require('./index');

/**
 * يعالج مصفوفة فواتير على دفعات بحجم محدّد
 * @param {Array} invoices - مصفوفة الفواتير
 * @param {number} batchSize - حجم الدفعة (الافتراضي 5)
 */
async function processBatch(invoices, batchSize = 5) {
  const results = [];

  for (let i = 0; i < invoices.length; i += batchSize) {
    const slice = invoices.slice(i, i + batchSize);
    const settled = await Promise.allSettled(
      slice.map((inv) => processInvoice(inv))
    );

    settled.forEach((res, idx) => {
      if (res.status === 'fulfilled') {
        results.push({ uuid: slice[idx].uuid, result: res.value });
      } else {
        results.push({ uuid: slice[idx].uuid, error: res.reason.message });
      }
    });
  }

  return results;
}

module.exports = { processBatch };

استخدمنا Promise.allSettled بدلًا من Promise.all لسبب مهم: allSettled ينتظر كل الطلبات سواء نجحت أو فشلت، بينما all يتوقّف عند أول فشل. في معالجة الفواتير لا تريد أن يُفشل خطأ في فاتورة واحدة معالجة بقية الدفعة. كل فاتورة تُعالَج باستقلالية وتُسجَّل نتيجتها فرديًا.

حجم الدفعة الافتراضي خمس فواتير قيمة محافظة آمنة. اضبطها حسب حدود المعدّل الفعلية للمنصة، وراقب ردود 429 لتعرف متى تخفّض الحجم أو تزيد التأخير بين الدفعات.

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

خلاصة عملية للمطوّر

بنينا في هذا الدليل تكاملًا كاملًا بلغة Node.js يغطّي كل مراحل إرسال الفاتورة الإلكترونية: ترويسة المصادقة عبر Buffer وBase64، حساب التجزئة عبر crypto، ترميز محتوى UBL، الإرسال عبر axios أو fetch، وتحليل الرد مع إعادة المحاولة الذكية. الكود مكتوب ليُلصق في مشروعك مع تعديل القيم الحسّاسة فقط.

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

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

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

المنطق نفسه متاح بلغات أخرى ضمن هذه السلسلة (PHP وPython وJava و.NET) لمن يبني تكامله بلغة مختلفة. وللعودة إلى المفاهيم الأساسية، ابدأ من المصادقة ثم نقاط نهاية API ثم إرسال الفاتورة.

مركز المساعدة

لم تجد ما تبحث عنه؟

لا تقلق، لدينا المزيد من أدوات المساعدة.

ندوات مباشرة يقدمها فريق قيود لمساعدتك في استخدام البرنامج بسهولة والرد على أسئلتك.

تعرّف على أحدث تحديثات فيود والتحسينات المستمرة والخصائص الجديدة في مكان واحد.

فريقنا جاهز لمساعدتك وتقديم الدعم الفوري لأي مشكلة تواجهها على مدار الساعة