هل يقبل مُحقِّق الهيئة رمزَ فاتورتك؟

مقياسٌ مفتوح يبني رموز QR فعلية بـ13 مكتبة، ثم يفكّها بايتاً بايت بفاكٍّ مستقلٍّ لا يستدعي كود أحد. 10 حالة، كلٌّ تحمل قاعدتها ومصدرَها: نصّ المواصفة، أو حسمٌ منشور في منتدى الهيئة نفسها.

91.7%

من 44,915 تنزيلٍ شهري قِيست لمكتبات ZATCA على npm تذهب إلى حزمٍ تُخفق في قاعدةٍ أو أكثر. ومن 11 مكتبةً قِيست، 2 فقط اجتازت الحالات كلّها.

وأشدُّ ما وُجد أثراً: 41,195 تنزيلٍ شهري في حزمٍ تكسر ترميز الطول عند 128 بايتاً — أي عند اسمٍ يقارب 64 حرفاً عربياً، وهو طولُ اسم منشأةٍ سعودية عادي — فالحرفُ العربي بايتان والمسافةُ بايت.

In English

The ZATCA spec sentence everyone copies is “The length shall be stored in one byte.” It stops being true at 128 bytes — and Arabic is two bytes per character in UTF-8, so a 64-character Arabic company name is 128 bytes. That is an ordinary Saudi trade name, not an edge case.

91.7% of the 44,915 monthly npm downloads measured here go to packages that fail at least one rule. Of 11 third-party packages, 2 pass every case. The rule is settled on ZATCA’s own forum, not by us.

We found the same bug in our own code first, in three places including our published checker. All were fixed before anyone else was measured, and every finding was filed as a public issue with a runnable reproduction.

Read this page in English → · method and the adapters we got wrong

وكودُنا نحن كان أحدها

ذهبنا نفحص حزمة غيرنا فوجدنا أن الخطأ عندنا: كنّا نكتب الطول في بايتٍ واحد، منقولاً حرفياً عن جملة المواصفة The length shall be stored in one byte. وهي تبسيطٌ ينكسر عند 128. أصلحناه في ثلاث نسخٍ عندنا — مكتبة PDF، وخادم MCP، وأداةُ الفحص المنشورة — قبل أن نقيس أحداً. والمقياس يرفض أن يُنتج صفحته أصلاً إن أخفق محرّكٌ من محرّكاتنا.

افحص رمزك أنت

الصق نصّ Base64 الذي يُنتجه مُشفّرك. لا يُرفع شيء — يعمل الفاكُّ نفسه الذي حكم على الجدول أدناه، في متصفّحك.

أو ثبّت واحدةً تفعلها أصلاً

الأسطر الأربعة أعلاه تكفي لمن يريد الرمز وحده. ومن يريد معه نصّاً عربياً ينجو في PDF — مبالغُ لا تنقلب وأسماءٌ لا تُبتر — فهاتان المكتبتان تفعلان الاثنين. ولم تُنشرا على npm بعد، وتُثبَّتان اليوم من إصدار:

npm install https://github.com/opusstudiohq-max/arabic-invoice-mcp/releases/download/libs-v0.2.0/fatura-zatca-0.2.0.tgz

وfatura-zatca تسحب nasq معها. وجُرّبت السلسلة من هذا العنوان في مشروعٍ نظيف: شكلُ الطول صحيح، والاسمُ سليمٌ بعد الفكّ، والمجاميع بالهللات.

النتيجة

المكتبة تنزيلات شهرية
قِيست 2026-09-01
النتيجة
@talha7k/zatca-qr 1,991 10/10
100%
@talha7k/zatca 1,729 10/10
100%
فاتورة نحن
نسختنا — أُصلحت بعد أن كشفها هذا المقياس نفسه
— 10/10
100%
خادم MCP — البناء المشحون نحن
البناء المشحون فعلاً لا نسخة المصدر. يُفحص هنا عمداً: من ينشر مقياساً يبدأ بنفسه
— 10/10
100%
zatca-xml-js 21,943 6/10
60%
@axenda/zatca 17,376 6/10
60%
@zatca/qr 503 6/10
60%
zatca-sdk
يبني المرحلة الثانية فقط — تُمرَّر حقول التوقيع صورياً وتُفحص الوسوم 1-5
300 6/10
60%
zatca-qr-tlv 123 6/10
60%
zatca-qr-generator 104 6/10
60%
zatca-simplified-invoice-sdk 10 6/10
60%
@pioneersoft/zatca-einvoice 646 5/10
50%
zatca
منشورة أيضاً باسم ‎@tatwerat/zatca
⚑ ترفض «2022-04-25T15:30:00Z» وتشترط كسور الثواني ‎.000Z‎ — تُقاد بها ليُقاس الترميز
190 2/10
20%

ما وُجد بالضبط

zatca-xml-js 21,943 تنزيلاً شهرياً

@axenda/zatca 17,376 تنزيلاً شهرياً

zatca-qr-tlv 123 تنزيلاً شهرياً

zatca-sdk 300 تنزيلاً شهرياً

zatca-qr-generator 104 تنزيلاً شهرياً

zatca-simplified-invoice-sdk 10 تنزيلاً شهرياً

@pioneersoft/zatca-einvoice 646 تنزيلاً شهرياً

@zatca/qr 503 تنزيلاً شهرياً

zatca 190 تنزيلاً شهرياً

وليست علّةَ جافاسكربت

الدرجاتُ أعلاه تقيس npm، لأنها ما يستطيع هذا المُشغِّل تشغيله. لكنّ العيب نفسه في أكثر مستودعات ZATCA نجوماً على GitHub، بثلاث لغاتٍ أخرى — ومنها مكتباتُ منصّاتٍ سعودية كبرى:

المستودعاللغةسطرُ الطول
SallaApp/ZATCAPHP pack("H*", sprintf("%02X", $len))
Saleh7/php-zatca-xmlPHP pack("H*", sprintf("%02X", $len))
mrsool/zatcaRuby @value.bytesize.chr
Haraj-backend/zatca-sdk-goGo buf.WriteByte(byte(len(val)))

وأربعتُها تعدّ البايتات لا الأحرف — أي تجاوزت الفخّ الذي يُسقط أكثر التطبيقات — ثم كتبت ذلك العدد في بايتٍ واحد. ومكتبةُ جو تقول القاعدة الخاطئة نصّاً:

// since the length could only be 1 byte, that means the maximum length for
// every field values is 255.
const maxValueLength = 255

فالخطأ ليس زلّةً في بيئةٍ بعينها، بل قراءةٌ شائعة لجملةٍ تقول «بايتٌ واحد» — ولذلك يبلغ 91.7% من التنزيلات المقيسة، ويبلغ قمّةَ GitHub معاً.

وهذه الأربعة قُرئت ولم تُشغَّل — لا PHP ولا روبي ولا جو على الجهاز الذي بنى هذه الصفحة. والاقتباساتُ حرفيةٌ من الفرع الافتراضي لكلٍّ منها. أما درجاتُ npm أعلاه فمُشغَّلة.

ما أُبلِغ به أصحابُه

مقياسٌ يجد عيباً ولا يُبلغ صاحبه نميمةٌ مُحكمة. فكل ما وُجد هنا رُفع مسألةً عامة، بالبرهان وسطور الإصلاح — وروابطها مفتوحة للحكم.

الحزمةالعيبالمسألة
zatca-xml-js الطول في بايتٍ واحد: 128 بايتاً تُكتب 0x80، و256 بايتاً تُعلن طولاً صفراً wes4m/zatca-xml-js/issues/61
@axenda/zatca الطول يمرّ بجولة UTF-8 فيصير محرف الاستبدال EF BF BD عند 128 بايتاً axenda/zatca/issues/19
zatca الطول بعدد الأحرف لا البايتات — كل اسمٍ عربي يُنتج رمزاً مشوّهاً abdo-host/ZATCA-JS/issues/1
zatca-qr-generator الطول في بايتٍ واحد؛ و256 بايتاً تُعلن طولاً صفراً بلا خطأ Wasim-Zaman/zatca-qr-generator/issues/1
zatca-sdk الطول في بايتٍ واحد عند 128 بايتاً بلا خطأ؛ ويرفض ما فوق 255 aashahin/zatca-sdk/issues/1
zatca-simplified-invoice-sdk الطول في بايتٍ واحد عند 128 بايتاً بلا خطأ؛ ويرفض ما فوق 255 sharahsa0-creator/zatca-simplified-invoice-sdk/issues/6
SallaApp/ZATCA PHP pack("H*", sprintf("%02X", $len)) — بايتٌ واحد دائماً؛ وأطولُ اسمٍ في اختباراتهم ستةُ بايتات SallaApp/ZATCA/issues/74
Saleh7/php-zatca-xml PHP الكود نفسه حرفاً بحرف — بايتٌ واحد للطول Saleh7/php-zatca-xml/issues/38
mrsool/zatca Ruby @value.bytesize.chr — البايتاتُ صحيحة، ثم chr تكتب بايتاً واحداً mrsool/zatca/issues/41
Haraj-backend/zatca-sdk-go Go تعليقُهم يقول «since the length could only be 1 byte» — والفجوةُ 128-255 تمرّ من حارسهم بلا اختبار Haraj-backend/zatca-sdk-go/issues/7

وما تعذّر الإبلاغ عنه:

الحالة × المكتبة

الحالةالقاعدة@talha7k/zatca-qr@talha7k/zatcaفاتورةخادم MCP — البناء المشحونzatca-xml-js@axenda/zatca@zatca/qrzatca-sdkzatca-qr-tlvzatca-qr-generatorzatca-simplified-invoice-sdk@pioneersoft/zatca-einvoicezatca
ascii-baseline
الحدّ الأدنى: حقول لاتينية قصيرة. من أخفق هنا لا يبني TLV أصلاً.
Q4 ✓✓✓✓✓✓✓✓✓✓✓✓✓
arabic-byte-length
«شركة» أربعة أحرف وثمانِ بايتات — أشهر فخّ يُسقط التطبيقات الساذجة.
Q1 ✓✓✓✓✓✓✓✓✓✓✓✓✗
arabic-63-chars
126 بايتاً — تحت الحدّ مباشرة، فبايتٌ واحد يحمله.
Q1 ✓✓✓✓✓✓✓✓✓✓✓✓✗
arabic-64-chars
128 بايتاً بالضبط — هنا ينكسر كل من قرأ نصّ المواصفة حرفياً. واسمُ منشأةٍ سعودية من 64 حرفاً عاديّ تماماً.
Q2 ✓✓✓✓✗✗✗✗✗✗✗✗✗
arabic-100-chars
200 بايتاً — داخل مدى 0x81 وبعيداً عن الحدّ، فلا يُنجي من الخطأ حظُّ الحدود.
Q2 ✓✓✓✓✗✗✗✗✗✗✗✗✗
arabic-128-chars
256 بايتاً — يعبر مدى البايت الواحد كلّه إلى 0x82.
Q3 ✓✓✓✓✗✗✗✗✗✗✗✗✗
roundtrip-long-arabic
اسمٌ من 64 حرفاً يخرج كما دخل. القاعدة نفسها من جهة القيمة لا من جهة الطول.
Q5 ✓✓✓✓✗✗✗✗✗✗✗✗✗
realistic-saudi-name
اسمٌ سعودي واقعي بطوله الطبيعي — لا سلسلةَ حرفٍ مكرّر.
Q5 ✓✓✓✓✓✓✓✓✓✓✓✓✗
amount-as-printed
«1000.00» على وجه الفاتورة يجب أن يكون «1000.00» في الرمز — لا «1000».
Q6 ✓✓✓✓✓✓✓✓✓✓✓✓✓
timestamp-verbatim
الطابع الزمني يدخل في المطابقة — وإعادةُ صياغته صامتاً تغيير قيمة.
Q7 ✓✓✓✓✓✓✓✓✓✓✓✗✗

القواعد ومصادرها

الحَكَم مصدرٌ مسمّى لا رأيُنا. فمن يخالف حالةً يخالف مصدرها، ويستطيع أن ينازع في المصدر نفسه علناً.

الرمزالقاعدة
Q1 الطول بعدد البايتات لا بعدد الأحرف
طول القيمة يُكتب بعدد بايتات ترميز UTF-8 لا بعدد الأحرف (§4.1). و«شركة» أربعة أحرف وثمانِ بايتات — فمن عدّ الأحرف أنتج رمزاً يفشل قارئه بلا رسالة تشخيص.
المصدر: ZATCA — Electronic Invoice Security Features Implementation Standards, §4.1 والجدول 3
Q2 الطول بقواعد BER عند 128 بايتاً
نصّ المواصفة «The length shall be stored in one byte» تبسيطٌ ينكسر عند 128: البايت الواحد يحمل حتى 127، وما فوقه يلزمه 0x81 ثم بايت الطول. والحسم من منتدى الهيئة نفسه بنصّ صاحب المشكلة بعد إصلاحها: «our code assumed that the maximum length of the value is 1 byte … we were not properly convert it to TLV value». وحدُّ 128 يبلغه اسمٌ عربي من 64 حرفاً — فالحرف بايتان.
Q3 الطول بقواعد BER فوق 255 بايتاً
ما جاوز 255 بايتاً يلزمه 0x82 ثم بايتان بترتيب كبير-أولاً. واسمٌ عربي من 128 حرفاً يبلغ 256 بايتاً.
Q4 ترتيب الوسوم 1 ثم 2 ثم 3 ثم 4 ثم 5
الجدول 3 يُرتّب الوسوم: اسم البائع، ثم الرقم الضريبي، ثم الطابع الزمني، ثم الإجمالي شامل الضريبة، ثم مبلغ الضريبة. والترتيب جزءٌ من البنية لا تفصيلٌ تجميلي.
المصدر: ZATCA — Electronic Invoice Security Features Implementation Standards, §4.1 والجدول 3
Q5 القيمة تصل كما كُتبت
الاسم المُدخَل يجب أن يخرج من الفكّ كما دخل — لا بتراً ولا تحريفاً في الترميز. وهذه القاعدة تكشف عيب الطول من الجهة الأخرى: طولٌ خاطئ يُنتج قيمةً مبتورة.
المصدر: ZATCA — Electronic Invoice Security Features Implementation Standards, §4.1 والجدول 3
Q6 المبلغ كما يُطبع على الفاتورة
الوسمان 4 و5 يحملان المبلغ نصّاً كما يُطبع على الفاتورة. فمن أعاد تنسيقه («1000.00» ⇐ «1000») خالف المطبوع، ورمزُه لا يطابق وجه الفاتورة.
المصدر: ZATCA — Electronic Invoice Security Features Implementation Standards, §4.1 والجدول 3
Q7 الطابع الزمني كما أُعطي
الوسم 3 طابعٌ زمني بصيغة ISO 8601 مع منطقة زمنية. ومن أعاد صياغته صامتاً غيّر قيمةً تدخل في المطابقة.
المصدر: ZATCA — Electronic Invoice Security Features Implementation Standards, §4.1 والجدول 3

أعِد تشغيله بنفسك

git clone https://github.com/opusstudiohq-max/arabic-invoice-mcp
cd arabic-invoice-mcp/zatca-qr-benchmark
npm install
node run.mjs --fetch && node build.mjs

الحالات في cases.json، والمحوّلات في run.mjs. ومن رأى محوّلاً يظلم مكتبته فليفتح مسألةً — أخطأنا في تسعةٍ من محوّلاتنا في التشغيل الأول، فأصلحناها قبل النشر، ولا نستبعد بقيّة.

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