> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# الفوترة الإلكترونية فاتورة ZATCA: كيف تربط نظامك
- URL: https://arabroot.io/articles/الفوترة-الالكترونية-فاتورة-zatca-كيف-تربط-نظامك/
- Published: 2026-10-27T16:51:30.000Z
- Updated: 2026-10-27T22:13:03.000Z
- Description: بنينا فاتورة مبسطة بصيغة XML ورمز QR بلغة Python، وتحققنا منهما بأداة الهيئة، وحسبنا الهاش بأنفسنا وجربنا الأخطاء الشائعة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, التقنية المالية والمدفوعات

لنفرض أن لديك شركة ناشئة تدير ستة مقاه صغيرة، وفريقها التقني كتب نظام نقاط البيع بنفسه قبل ثلاث سنوات: شاشة على جهاز لوحي، وطابعة إيصالات حرارية، وقاعدة بيانات في السحابة تجمع مبيعات الفروع. والنظام يطبع منذ 2021 فاتورة مبسطة عليها رمز QR، ويمسحه بعض الزبائن بتطبيق الهيئة فيظهر اسم المقهى والمبلغ والضريبة، وكان الجميع راضين. ثم وصل إشعار من هيئة الزكاة والضريبة والجمارك ZATCA بأن الشركة ضمن مجموعة مستهدفة في مرحلة الربط، وأمامها ستة أشهر. وفتح المهندس المسؤول الوثائق، فوجد كلمات لم يرها في كود الإيصال من قبل: UBL 2.1، وختم تشفيري Cryptographic Stamp، وهاش الفاتورة السابقة PIH، وعداد الفواتير ICV، وتوقيع XAdES، ورمز QR بتسعة حقول بدل خمسة.

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

سوف نناقش في هذا المقال:

- ما هي الفوترة الإلكترونية فاتورة، والفرق بين الفاتورة الضريبية والمبسطة، وبين الاعتماد Clearance والإبلاغ Reporting.
- الوثائق الرسمية التي تحتاجها، وأداة التحقق SDK وما تقوله الهيئة عن نتيجتها.
- الفاتورة المبسطة بصيغة UBL من الداخل: الرأس والعداد والهاش السابق والبائع والإجماليات والبنود.
- المختبر: بناء الفاتورة، وأول تحقق بخمسة أخطاء، وخطأ لا تتوقعه سببه الكسور العشرية في قواعد التحقق نفسها.
- التوقيع والهاش: ما الذي يدخل في الهاش وما الذي يستثنى، وكيف حسبناه بأنفسنا وطابق الأداة.
- رمز QR بصيغة TLV، وخطأ الطول بالحروف لا بالبايتات، وما وجدناه في الحقول 7 و8 و9.
- سلسلة الهاش، والتعديل بعد التوقيع، وما يطلبه الدليل من النظام نفسه، وأخطاء شائعة وقائمة تحقق.

## ما هي الفوترة الإلكترونية فاتورة؟

تعرف الهيئة في صفحة [ما هي الفوترة الإلكترونية](https://zatca.gov.sa/en/E-Invoicing/Introduction/Pages/What-is-e-invoicing.aspx?ref=arabroot.io) الفاتورة الإلكترونية بأنها فاتورة ضريبية تنشأ بصيغة إلكترونية منظمة Structured عبر وسيلة إلكترونية، وتقول صراحة إن الفاتورة الورقية التي تحول إلى صيغة إلكترونية بالنسخ أو المسح الضوئي ليست فاتورة إلكترونية. أي أن ملف PDF مطبوعاً من Word لا يكفي، والمقصود بيانات يقرؤها نظام آخر دون إنسان. وتفرق الصفحة نفسها بين نوعين:

- **الفاتورة الضريبية Tax Invoice**: تصدر عادة من منشأة لمنشأة B2B، وتحتوي كل عناصر الفاتورة الضريبية، ومنها بيانات المشتري.
- **الفاتورة الضريبية المبسطة Simplified Tax Invoice**: تصدر عادة من منشأة لمستهلك B2C، وهي إيصال المقهى في قصتنا، والمشتري فيها قد يبقى بلا اسم.

والتطبيق على مرحلتين بحسب الصفحة نفسها: مرحلة الإصدار Generation المطبقة منذ 4 ديسمبر 2021 على كل المكلفين المقيمين، وفيها تصدر الفاتورة من حل إلكتروني متوافق مع حقول إضافية، ومرحلة الربط والتكامل Integration التي بدأت من 1 يناير 2023 على موجات، وفيها يجب أن يربط الحل بأنظمة الهيئة وأن تصدر الفواتير بالصيغة المطلوبة. وقرار الهيئة بشأن [ضوابط ومتطلبات تطبيق الفوترة](https://zatca.gov.sa/en/E-Invoicing/Introduction/LawsAndRegulations/Documents/20230519%5FE-Invoicing%20Implementation%20Resolution%20English.pdf?ref=arabroot.io) ينص على أن الهيئة تحدد المجموعات المستهدفة ومراحل الربط، وتبلغ كل مجموعة بإجراءات الربط قبل موعدها بستة أشهر على الأقل، وهذا هو الإشعار الذي وصل المقهى.

## لماذا تريد الهيئة بيانات منظمة لا ورقة؟

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

ومن جهة المهندس، يعني ذلك أن الفاتورة لم تعد «مستنداً يطبع»، وإنما رسالة بين نظامين، مثل رسائل البنوك التي شرحناها في [رسائل البنوك: ISO 8583 وISO 20022](https://arabroot.io/articles/%D8%B1%D8%B3%D8%A7%D8%A6%D9%84-%D8%A7%D9%84%D8%A8%D9%86%D9%88%D9%83-iso-8583-%D9%84%D9%84%D8%A8%D8%B7%D8%A7%D9%82%D8%A7%D8%AA-%D9%88-iso-20022-%D9%84%D9%84%D8%AA%D8%AD%D9%88%D9%8A%D9%84%D8%A7%D8%AA/): لها معيار بأسماء عناصر، ومخطط يتحقق من البنية، وقواعد عمل تتحقق من المعنى، والطرف الآخر يرفض ما لا يطابق. والفرق أن رسالة البنك تذهب لبنك آخر، ورسالة الفاتورة تذهب لجهة تملك سلطة فرض الغرامات، فالخطأ فيها أغلى.

## الاعتماد أم الإبلاغ؟ نوع الفاتورة يحدد المسار

أهم فرق هندسي بين النوعين في المرحلة الثانية هو متى تصل الفاتورة إلى الهيئة، والصورة التالية تبينه:

![نظام الفوترة EGS يولد XML ويوقعه، ومنه مساران: الفاتورة الضريبية B2B تذهب للهيئة أولاً للاعتماد Clearance ثم تسلم للمشتري بعد الاعتماد عبر منصة فاتورة FATOORA، والفاتورة المبسطة B2C تسلم للعميل فوراً بختم ورمز QR ثم ترسل للهيئة خلال 24 ساعة للإبلاغ Reporting، وسهم منحن يبين أن العميل يأخذ الفاتورة فوراً والهيئة تستلمها لاحقاً](https://arabroot.io/content/images/2026/10/zatca-einvoicing-01-clearance-reporting.webp)

في المبسطة لا ينتظر الزبون الهيئة، ولهذا يحتاج نظامك أن يختم الفاتورة بنفسه

وتشرح [الإرشادات التفصيلية للفوترة الإلكترونية](https://zatca.gov.sa/en/E-Invoicing/Introduction/Guidelines/Documents/E-Invoicing%5FDetailed%5F%5FGuideline.pdf?ref=arabroot.io) (الإصدار 2، مايو 2023) المسارين: في الاعتماد Clearance ترسل الفاتورة الضريبية للهيئة لتتحقق منها وتضع ختمها التشفيري، ثم تسلم للمشتري، وتنص صراحة على أن الاعتماد لا ينطبق على الفواتير المبسطة. أما الإبلاغ Reporting فهو مشاركة الفواتير المبسطة وإشعاراتها مع الهيئة بعد إصدارها، ويجب أن ترسل كلها إلى منصة فاتورة خلال 24 ساعة من إصدارها، بصيغة XML عبر الواجهات البرمجية API. وتقول الإرشادات أيضاً إن الفاتورة المبسطة في المرحلة الثانية يجب أن تختم بختم تشفيري باستخدام معرف الختم CSID الذي تصدره الهيئة، وأن تحمل رمز QR متوافقاً مع المرحلة الثانية بتسعة حقول بصيغة TLV مرمزة بـ base64.

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

## الوثائق التي تحتاجها، وأداة SDK

قبل أي كود جمع المهندس الوثائق من صفحات الهيئة للمطورين، وكلها بتاريخ 19 مايو 2023:

| الوثيقة                                                                                                                                                                                                                         | ما فيها                                              |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| [معيار تطبيق صيغة XML للفاتورة الإلكترونية](https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20230519%5FZATCA%5FElectronic%5FInvoice%5FXML%5FImplementation%5FStandard%5F%20vF.pdf?ref=arabroot.io)، الإصدار 1.2 | العناصر وقواعد العمل BR وBR-KSA ورموز أنواع الفواتير |
| [معايير تطبيق خصائص الأمان](https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20230519%5FZATCA%5FElectronic%5FInvoice%5FSecurity%5FFeatures%5FImplementation%5FStandards%5FvF.pdf?ref=arabroot.io)، الإصدار 1.2   | الختم التشفيري والهاش والهاش السابق ورمز QR          |
| الإرشادات التفصيلية، الإصدار 2                                                                                                                                                                                                  | الاعتماد والإبلاغ والتخزين والوظائف الممنوعة         |
| قرار الضوابط والمتطلبات                                                                                                                                                                                                         | المراحل والمجموعات والالتزامات                       |

وفي صفحة [تنزيل SDK](https://zatca.gov.sa/en/E-Invoicing/SystemsDevelopers/ComplianceEnablementToolbox/Pages/DownloadSDK.aspx?ref=arabroot.io) توفر الهيئة أداة سطر أوامر بلغة Java تتحقق من الفواتير والإشعارات، وتوقعها، وتحسب هاشها، وتنشئ رمز QR وطلبات الواجهة البرمجية. والملف الذي يشير إليه رابط التنزيل في هذه الصفحة هو `zatca-envoice-sdk-203.zip` بحجم 40.9 ميجابايت، وفيه الأداة بالإصدار 3.0.8 وتواريخ ملفاتها مايو 2022، ومعها مخططات UBL 2.1، وقواعد Schematron للمعيار الأوروبي EN 16931 ولقواعد المملكة بتاريخ 2021-08-19، وشهادة اختبار ومفتاحها الخاص. ولاحظ أن هذه القواعد أقدم من الإصدار 1.2 من المعيار، وسوف يظهر أثر ذلك في المختبر.

وتضع الهيئة في نفس الصفحة ملاحظتين يجب أن يقرأهما كل فريق: أن الملفات تعد متوافقة بعد أن تمر بالتحقق في الأداة، وأن النجاح في هذا التحقق لا يعني أن الهيئة اعتمدت الفاتورة، ولا يعفي من مسؤولية الالتزام بالمتطلبات. أي أن الأداة هي خط الدفاع الأول في بيئتك، والمرجع النهائي هو منصة فاتورة. وللاختبار مع الهيئة نفسها بيئة اختبار Sandbox في بوابة المطورين، ويشرح [دليل بوابة المطورين](https://zatca.gov.sa/en/E-Invoicing/Introduction/Guidelines/Documents/DEVELOPER-PORTAL-MANUAL.pdf?ref=arabroot.io) أنها تحتاج حساباً في البوابة، وفيها تجرب التسجيل Onboarding والحصول على معرف ختم للاختبار وإرسال الفواتير، ولم نستخدمها في هذا المختبر لأنه يعمل كله دون أي حساب أو بيانات مكلف حقيقي.

## الفاتورة المبسطة من الداخل

الفاتورة ملف XML بمعيار UBL 2.1، وهو معيار مفتوح من OASIS لمستندات الأعمال، والهيئة لم تخترع صيغة جديدة، وإنما بنت على UBL وعلى قواعد المعيار الأوروبي EN 16931، ثم أضافت قواعدها الخاصة التي تبدأ بـ BR-KSA. والصورة التالية تبين أجزاء الفاتورة المبسطة بعد توقيعها:

![أجزاء الفاتورة المبسطة من الأعلى: ext:UBLExtensions للتوقيع XAdES يضيفه التوقيع، ثم ProfileID وID وUUID وIssueDate وIssueTime لرأس الفاتورة والنوع 388 بالاسم 0200000، ثم AdditionalDocumentReference ICV لعداد الفواتير، ثم AdditionalDocumentReference PIH لهاش الفاتورة السابقة، ثم AdditionalDocumentReference QR لرمز QR بصيغة TLV يضيفه التوقيع، ثم cac:Signature لمرجع التوقيع، ثم AccountingSupplierParty للبائع بالرقم الضريبي والسجل والعنوان الوطني، ثم TaxTotal مرتين وLegalMonetaryTotal للضريبة والإجماليات، ثم InvoiceLine للبنود، والأجزاء الحمراء لا تدخل في الهاش والباقي يدخل بعد التوحيد C14N](https://arabroot.io/content/images/2026/10/zatca-einvoicing-02-anatomy.webp)

ثلاثة أجزاء يضيفها التوقيع ولا تدخل في الهاش، والباقي هو الفاتورة نفسها

- **نوع الفاتورة**: الرمز 388 للفاتورة الضريبية، والصفة `name="0200000"` تقول إنها مبسطة، فالخانتان الأوليان 02، كما في الجدول الذي يذكر أن الفاتورة الضريبية المبسطة رمزها 388 ونوعها الفرعي 02\. والإشعار الدائن المبسط 381 والمدين 383 بنفس النوع الفرعي.
- **ProfileID**: قيمته `reporting:1.0`، أي أن هذه الفاتورة تسلك مسار الإبلاغ.
- **UUID**: معرف فريد لكل فاتورة إلى جانب رقمها الذي يراه الزبون.
- **ICV**: عداد الفواتير Invoice Counter Value، رقم يزيد بواحد مع كل فاتورة يصدرها الجهاز، والإرشادات تطلب ألا يكون تصفيره متاحاً لأي مستخدم.
- **PIH**: هاش الفاتورة السابقة Previous Invoice Hash، وهو ما يربط الفواتير في سلسلة كما سنرى.
- **البائع**: رقم السجل التجاري بنوع `CRN`، والعنوان الوطني كاملاً، والرقم الضريبي من 15 خانة، والاسم القانوني بالعربية.
- **الإجماليات**: عنصرا `TaxTotal`، الأول بتفصيل الضريبة بحسب الفئة والنسبة، والثاني بالمبلغ وحده، ثم `LegalMonetaryTotal` بالصافي والإجمالي مع الضريبة والمستحق.

## المختبر: نبني الفاتورة بأنفسنا

كتبنا الفاتورة بمكتبة lxml في Python، لأن الهدف أن نفهم كل عنصر لا أن نعتمد على مكتبة تخفيه. والبيانات كلها أمثلة: الرقم الضريبي `399999999900003` ورقم السجل والعنوان ليست لمكلف حقيقي، والمقهى اسمه «مقهى المثال التجريبي». وهذا أهم جزء في الملف:

```python
def build(number, icv, pih=FIRST_PIH, issue_date="2026-10-27", issue_time="10:15:00",
          seller="مقهى المثال التجريبي", lines=(("قهوة عربية دلة", 1, "45.00"), ("تمر خلاص 500 غرام", 2, "15.00"))):
    inv = etree.Element("Invoice", nsmap=NS)
    el(inv, CBC + "ProfileID", "reporting:1.0")
    el(inv, CBC + "ID", number)
    el(inv, CBC + "UUID", str(uuid.uuid5(uuid.NAMESPACE_URL, f"arabroot-lab/{number}")))
    el(inv, CBC + "IssueDate", issue_date)
    el(inv, CBC + "IssueTime", issue_time)
    el(inv, CBC + "InvoiceTypeCode", "388", name="0200000")          # 388 = tax invoice, 02 = simplified
    el(inv, CBC + "DocumentCurrencyCode", "SAR")
    el(inv, CBC + "TaxCurrencyCode", "SAR")
    r = el(inv, CAC + "AdditionalDocumentReference"); el(r, CBC + "ID", "ICV"); el(r, CBC + "UUID", icv)   # invoice counter
    r = el(inv, CAC + "AdditionalDocumentReference"); el(r, CBC + "ID", "PIH")                             # previous invoice hash
    el(el(r, CAC + "Attachment"), CBC + "EmbeddedDocumentBinaryObject", pih, mimeCode="text/plain")

    sp = el(el(inv, CAC + "AccountingSupplierParty"), CAC + "Party")
    el(el(sp, CAC + "PartyIdentification"), CBC + "ID", "1010010000", schemeID="CRN")
    a = el(sp, CAC + "PostalAddress")
    for tag, v in (("StreetName", "شارع التجربة"), ("BuildingNumber", "1234"), ("PlotIdentification", "5678"), ("CitySubdivisionName", "حي المثال"),
                   ("CityName", "الرياض"), ("PostalZone", "12345")):
        el(a, CBC + tag, v)
```

وبقية الملف يحسب الصافي والضريبة بالنوع `Decimal` مع التقريب إلى هللتين، ويكتب عنصري الضريبة والإجماليات والبنود. ولاحظ قيمة `FIRST_PIH` في رأس الملف: `NWZlY2Vi...OQ==`، وهي القيمة التي تحملها أول فاتورة في السلسلة لأنه لا توجد فاتورة قبلها، وهي نفس القيمة الموجودة في ملف `pih.txt` الذي يأتي مع الأداة، وفك ترميزها يعطي هاش SHA-256 للنص «0» مكتوباً بالأرقام الست عشرية.

وشغلنا Python في Container من python:3.13.17-slim، والأداة في Container ثان من eclipse-temurin:11.0.32\_9-jre-noble لأنها تحتاج Java، والاثنان يتشاركان مجلد العمل، ومجلد الأداة مركب للقراءة فقط:

```bash
docker run -d --name zatca-java -v "$PWD/sdk/zatca-envoice-sdk-203:/sdk:ro" -v "$PWD/work:/work" -w /work \
  -e SDK_CONFIG=/work/config.json eclipse-temurin:11.0.32_9-jre-noble sleep infinity
docker run -d --name zatca-py -v "$PWD/work:/work" -w /work python:3.13.17-slim sleep infinity
docker exec zatca-py pip install -q lxml==6.0.2 cryptography==46.0.3 qrcode==8.2 pillow==12.0.0
```

والأداة تأتي بسكربت `fatoora` وسكربت تثبيت يحتاجان jq لكتابة ملف الإعدادات، فبدل ذلك كتبنا ملف `config.json` بمسارات الـ Container مباشرة، وسكربتاً صغيراً يشغل نفس أمر Java الذي يشغله `fatoora`، ويحذف رسائل الترحيب ورسائل التحذير من Java 11:

```bash
#!/bin/sh
# run the ZATCA SDK CLI (the same java command the fatoora script runs) and keep only its result lines
java -Djdk.sunec.disableNative=false -jar /sdk/Apps/cli-3.0.8-jar-with-dependencies.jar --globalVersion 3.0.8 -certpassword 123456789 "$@" 2>&1 \
  | grep -vE '^WARNING|^\*|^This SDK|^It (can|returns|checks)|^$' | sed -E 's/^[0-9-]+ [0-9:,]+ //'
```

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

### أول تحقق: خمسة أخطاء

بنينا النسخة الأولى من الفاتورة، وكان فيها بند تمر بكميتين وسعر 18 ريالاً، ولم نضع الرقم الإضافي في العنوان، وتحققنا منها بالأمر `sh sdk.sh -validate -invoice invoice-v1-unsigned.xml` قبل أي توقيع، والمخرج سوف يكون كما يلي:

```text
[INFO] ValidationProcessorImpl - [XSD] validation result : PASSED
[INFO] ValidationProcessorImpl - [EN] validation result : PASSED
[INFO] ValidationProcessorImpl - [KSA] validation result : FAILED
[ERROR] ValidationProcessorImpl - ksa validation errors :
[ERROR] ValidationProcessorImpl - CODE : BR-KSA-09, MESSAGE : [BR-KSA-09]-Seller address must contain additional number (KSA-23), street name (BT-35), building number (KSA-17), postal code (BT-38), city (BT-37), Neighborhood (KSA-3), country code (BT-40). ...
[ERROR] ValidationProcessorImpl - CODE : BR-KSA-60, MESSAGE : [BR-KSA-60]-Cryptographic stamp (KSA-15) must exist in simplified tax invoices and associated credit notes and debit notes (KSA-2, position 1 and 2 = 02).
[ERROR] ValidationProcessorImpl - CODE : BR-KSA-50, MESSAGE : [BR-KSA-50]-The line VAT amount (KSA-11) must be Invoice line net amount (BT-131) x (Line VAT rate (BT-152)/100).
[ERROR] ValidationProcessorImpl - CODE : BR-KSA-64, MESSAGE : [BR-KSA-64]-Seller Address Additional number (KSA-23) must be 4 digits.
[ERROR] ValidationProcessorImpl - CODE : BR-KSA-27, MESSAGE : [BR-KSA-27]-The document must contain aa QR code (KSA-14), and this code must be base64Binary. ...
[INFO] InvoiceValidationService -  *** GLOBAL VALIDATION RESULT = FAILED
```

في المخرج أعلاه لاحظ التالي:

- **ثلاث طبقات بالترتيب**: XSD يتحقق من البنية، أي أن العناصر في أماكنها وبأنواعها، ومرت. ثم EN يطبق قواعد المعيار الأوروبي، مثل أن مجموع البنود يساوي الصافي، ومرت. ثم KSA يطبق قواعد المملكة، وفيها كل الأخطاء.
- **BR-KSA-09 وBR-KSA-64**: العنوان الوطني يحتاج الرقم الإضافي Additional Number، وهو في UBL العنصر `PlotIdentification` بأربعة أرقام. وهذا خطأ يقع فيه كل من ينقل عنوان البائع من نظام لا يعرف العنوان الوطني.
- **BR-KSA-60 وBR-KSA-27**: الفاتورة المبسطة يجب أن تحمل ختماً تشفيرياً ورمز QR، وهذا متوقع لأننا لم نوقع بعد، ويؤكد أن المبسطة غير المختومة ليست فاتورة صالحة في هذه المرحلة.
- **BR-KSA-50**: ضريبة البند يجب أن تساوي صافي البند في نسبة الضريبة، وهذا ما فعلناه بالضبط: 36.00 في 15% تساوي 5.40\. فلماذا الخطأ؟

### خطأ لم نرتكبه: 36 في 0.15 لا تساوي 5.40

فتحنا ملف القواعد `20210819_ZATCA_E-invoice_Validation_Rules.xsl` وبحثنا عن القاعدة، فوجدنا الشرط كما هو:

```text
test="not((cbc:TaxAmount) = (../cbc:LineExtensionAmount) * (../cac:Item/cac:ClassifiedTaxCategory/cbc:Percent div 100))"
```

الشرط يضرب المبلغ في 15 مقسومة على 100، ويقارن الناتج بالضريبة المكتوبة بالمساواة التامة، والأرقام هنا تقرأ من النص كأعداد عشرية مزدوجة Double، أي بنفس طريقة الكسور العشرية في المعالج التي شرحناها في [الأرقام العشرية Floating Point من الداخل](https://arabroot.io/articles/%D8%A7%D9%84%D8%A3%D8%B1%D9%82%D8%A7%D9%85-%D8%A7%D9%84%D8%B9%D8%B4%D8%B1%D9%8A%D8%A9-floating-point-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/). والرقم 0.15 لا يمثل في النظام الثنائي بدقة، فجربنا الحساب نفسه في Python:

```text
45 6.75 True
36 5.3999999999999995 False
40 6.0 True
12 1.7999999999999998 False
```

أي أن 45 في 0.15 تعطي 6.75 تماماً فيمر البند الأول، و36 في 0.15 تعطي 5.3999999999999995 فلا تساوي 5.40 ويفشل البند الثاني، رغم أن الضريبة صحيحة إلى الهللة. وللتأكد أن هذا هو السبب وحده، بنينا فاتورة ثانية لا يختلف فيها إلا هذا البند، ووقعناها حتى تختفي أخطاء الختم، فبقي خطأ واحد فقط:

```text
== b. a line of 2 x 18.00: VAT 5.40
[INFO] ValidationProcessorImpl - [XSD] validation result : PASSED
[INFO] ValidationProcessorImpl - [EN] validation result : PASSED
[INFO] ValidationProcessorImpl - [KSA] validation result : FAILED
[ERROR] ValidationProcessorImpl - CODE : BR-KSA-50, MESSAGE : [BR-KSA-50]-The line VAT amount (KSA-11) must be Invoice line net amount (BT-131) x (Line VAT rate (BT-152)/100).
```

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

### بعد الإصلاح والتوقيع: كل الطبقات تمر

أضفنا الرقم الإضافي للعنوان، وغيرنا البند، ثم وقعنا الفاتورة بالأداة نفسها، وهي توقعها بشهادة الاختبار التي تأتي معها، ثم تحققنا من الملف الموقع:

```bash
sh sdk.sh -sign -invoice invoice-unsigned.xml -signedInvoice invoice-signed.xml
sh sdk.sh -validate -invoice invoice-signed.xml
```

```text
[INFO] InvoiceSigningService - invoice has been signed successfully
[INFO] InvoiceSigningService -  *** INVOICE HASH = kvihr8CX4oLlHSfNBFg2PetotT9CGkNkbOGixrwz3y4=

[INFO] ValidationProcessorImpl - [XSD] validation result : PASSED
[INFO] ValidationProcessorImpl - [EN] validation result : PASSED
[INFO] ValidationProcessorImpl - [KSA] validation result : PASSED
[INFO] ValidationProcessorImpl - [QR] validation result : PASSED
[INFO] ValidationProcessorImpl - [SIGNATURE] validation result : PASSED
[INFO] ValidationProcessorImpl - [PIH] validation result : PASSED
[INFO] InvoiceValidationService -  *** GLOBAL VALIDATION RESULT = PASSED
```

ظهرت ثلاث طبقات جديدة بعد التوقيع: QR يتحقق أن الرمز يطابق الملف، وSIGNATURE يتحقق من التوقيع، وPIH يتحقق أن هاش الفاتورة السابقة هو المتوقع. والسطر المهم في التوقيع هو هاش الفاتورة `kvihr8CX...`، فهو بصمة الفاتورة كلها، وسوف نحسبه الآن بأنفسنا.

## الهاش: ما الذي يدخل فيه بالضبط؟

تقول معايير تطبيق خصائص الأمان في الإصدار 1.2 إن التوقيع في ملفات XML يتبع XAdES، وإن الهاش يحسب بـ SHA-256 بعد تحويلات Transforms محددة: حذف `ext:UBLExtensions` الذي يحمل التوقيع نفسه، وحذف `cac:Signature`، وحذف مرجع رمز QR، ثم توحيد النص Canonicalization بطريقة C14N 1.1، وهي طريقة تكتب XML بشكل واحد ثابت، فترتيب الصفات وشكل علامات الاقتباس لا يغير الهاش. والسبب في الحذف منطقي: التوقيع والرمز يحتويان الهاش، فلا يمكن أن يدخلا في حسابه. وكتبنا الدالة كما تصفها الوثيقة:

```python
def keep_tail(e):
    """lxml drops the text after an element (its tail) with the element; the XPath transform keeps that text"""
    if e.tail:
        prev = e.getprevious()
        if prev is not None:
            prev.tail = (prev.tail or "") + e.tail
        else:
            e.getparent().text = (e.getparent().text or "") + e.tail

def invoice_hash(doc):
    """the transform of the Security Features standard: drop UBLExtensions, Signature and the QR reference, canonicalise, SHA-256"""
    d = etree.fromstring(etree.tostring(doc))
    for path in ("//ext:UBLExtensions", "//cac:Signature", "//cac:AdditionalDocumentReference[cbc:ID='QR']"):
        for e in d.xpath(path, namespaces=N):
            keep_tail(e)
            e.getparent().remove(e)
    c14n = etree.tostring(d, method="c14n")
    return hashlib.sha256(c14n).digest(), c14n
```

والدالة `keep_tail` لم تكن في النسخة الأولى، وقصتها درس يستحق أن تعرفه. ففي التشغيل الأول خرج هاشنا `Y+KeGcxz...` وهاش الأداة `kvihr8CX...`، أي أنهما مختلفان تماماً. فحسبنا الهاش من الملف غير الموقع، فطابق الأداة، فعرفنا أن الخطأ في الحذف لا في الحساب. وقارنا النصين الموحدين سطراً سطراً، فوجدنا الفرق في المسافات فقط: قبل `ProfileID` سطر جديد ومسافتان في نسخة، وليس في الأخرى. والسبب أن مكتبة lxml عندما تحذف عنصراً تحذف معه النص الذي يأتي بعده، أي المسافات والسطر الجديد، بينما تحويل XPath في المعيار يحذف العنصر ويبقي النص الذي حوله. ومسافة واحدة في ملف من خمسة آلاف بايت تغير الهاش كله، وهذه طبيعة دوال الهاش. وبعد الإصلاح طابق الهاش، كما في الفقرة التالية.

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

## رمز QR من الداخل: TLV

تصف الفقرة 4 من معايير خصائص الأمان رمز QR بأنه حقول بصيغة Tag-Length-Value واختصاراً TLV، مرمزة بـ base64، وطولها حتى 700 حرف: لكل حقل بايت للرقم Tag، ثم بايت للطول، والطول هو عدد بايتات القيمة بعد ترميزها UTF-8، ثم القيمة نفسها. والحقول من 1 إلى 5 مطبقة من 4 ديسمبر 2021: اسم البائع، ورقمه الضريبي، ووقت الفاتورة بصيغة ISO 8601 مثل `2022-02-21T12:13:57Z`، والإجمالي مع الضريبة، ومجموع الضريبة. ومن 1 يناير 2023 تضاف الحقول من 6 إلى 9: هاش الفاتورة، وتوقيع ECDSA للهاش، والمفتاح العام، وللفواتير المبسطة توقيع الهيئة على شهادة الختم. والصورة التالية تبين أول حقلين بالبايتات الحقيقية من المختبر:

![بايتات رمز QR من اليمين: 01 للرقم Tag، ثم 26 للطول أي 38، ثم d9 85 d9 82 للقيمة بترميز UTF-8، ثم 02 للرقم Tag، ثم 0f أي 15، ثم 33 39 39 للرقم الضريبي 399999999900003، وتحتها الخطأ الشائع: الطول بعدد الحروف len(name) يساوي 20 بدل البايتات 38، فيقرأ القارئ 20 بايتاً فينقطع الاسم في منتصف حرف «مقهى المثا�»، ثم يقرأ بايتاً عربياً كأنه Tag فيخرج tag 132 بطول 32 ثم بيانات تتجاوز النهاية](https://arabroot.io/content/images/2026/10/zatca-einvoicing-03-tlv.webp)

الطول بالبايتات، والاسم العربي يأخذ ضعف عدد حروفه تقريباً

وبنينا الحقول الخمسة الأولى بدالة من أربعة أسطر:

```python
def tlv(fields):
    out = b""
    for tag, value in fields:
        v = value if isinstance(value, bytes) else str(value).encode("utf-8")
        out += bytes([tag, len(v)]) + v                 # tag in one byte, length of the UTF-8 bytes in one byte
    return out
```

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

```text
== 1. tags 1 to 5 built by hand
  seller name: 20 characters, 38 UTF-8 bytes
  first bytes: 01 26 d9 85 d9 82 d9 87 d9 89 20 d8
  base64: ASbZhdmC2YfZiSDYp9mE2YXYq9in2YQg2KfZhNiq2KzYsdmK2KjZigIPMzk5OTk5OTk5OTAwMDAzAxQyMDI2LTEwLTI3VDEwOjE1OjAwWgQFODYuMjUFBTExLjI1 (124 chars)
== 2. the QR the SDK wrote into the signed invoice
  base64 length 396 chars (the standard allows up to 700), 9 tags
  tag 1 seller name       38 bytes  مقهى المثال التجريبي
  tag 2 VAT number        15 bytes  399999999900003
  tag 3 timestamp         20 bytes  2026-10-27T10:15:00Z
  tag 4 total with VAT     5 bytes  86.25
  tag 5 VAT total          5 bytes  11.25
  tag 6 invoice hash      44 bytes  kvihr8CX4oLlHSfNBFg2PetotT9CGkNkbOGixrwz3y4=
  tag 7                   88 bytes  3056301006072a8648ce3d02...
  tag 8                   32 bytes  25b3a0b8169ddf5d5d284f6d...
  tag 9                   32 bytes  4785739173f752f0c586a568...
  tags 1-5 identical to ours: True
== 3. checking tag 6 ourselves
  canonical XML hashed: 4978 bytes
  our SHA-256 (base64): kvihr8CX4oLlHSfNBFg2PetotT9CGkNkbOGixrwz3y4=
  tag 6 equals base64 of our hash: True
```

في المخرج أعلاه لاحظ التالي:

- **الاسم 20 حرفاً و38 بايتاً**: كل حرف عربي بايتان في UTF-8، والمسافات بايت واحد، ولهذا الطول في البايت الثاني `26`، أي 38 بالست عشري. وتذكر أن الطول بايت واحد، أي 255 بايتاً على الأكثر لكل حقل، وهذا يكفي اسماً عربياً بحوالي 125 حرفاً فقط.
- **الحقول الخمسة متطابقة**: ولكن بعد إصلاح واحد، ففي التشغيل الأول كتبنا الوقت دون حرف `Z` في آخره، فلم تتطابق، والأداة تكتبه بالـ Z كما في مثال المعيار، أي بتوقيت UTC. وهذا يعني أن وقت الفاتورة في الرمز يجب أن يكون بتوقيت UTC، فإذا كتبت فيه الساعة بتوقيت السعودية ثم أضفت Z فأنت تقول إن الفاتورة صدرت قبل ثلاث ساعات من وقتها الحقيقي.
- **الحقل 6 هو الهاش نفسه**: 44 بايتاً هي نص base64 للهاش، وطابق ما حسبناه من 4,978 بايتاً من XML الموحد، أي أننا فهمنا التحويلات كما تفهمها الأداة.
- **الرمز 396 حرفاً**: تحت حد 700 حرف، ولكنه أكبر بثلاث مرات من رمز المرحلة الأولى بخمسة حقول، 124 حرفاً. وهذا مهم لطابعة الإيصالات كما سنرى.

### ماذا وجدنا في الحقول 7 و8 و9؟

بحسب جدول الحقول في الإصدار 1.2 من المعيار، الحقل 7 هو توقيع ECDSA للهاش، والحقل 8 المفتاح العام، والحقل 9 توقيع الهيئة على شهادة الختم. ولكن الأطوال في المخرج لا تشبه ذلك، فالحقل 7 طوله 88 بايتاً ويبدأ بـ `3056301006072a8648ce3d02`، وهي بداية مفتاح عام بصيغة DER. فتحققنا من كل حقل بأنفسنا بمكتبة cryptography:

```text
== 4. what tags 7, 8 and 9 hold in this SDK version
  tag 7 is the certificate's public key (DER): True | curve secp256k1
  test certificate: CN=TST--970560040-312345678900003,OU=TST,O=3123456789,C=SA | valid until 2024-04-18
  tags 8 and 9 as (r, s) verify an ECDSA-SHA256 signature over the invoice hash: True
```

أي أن هذا الإصدار من الأداة كتب في الحقل 7 المفتاح العام للشهادة، وفي الحقلين 8 و9 جزأي التوقيع r وs كل منهما 32 بايتاً، والتوقيع صحيح بالمفتاح على منحنى secp256k1 فوق بايتات الهاش. وهذا يختلف عن ترتيب الجدول في المعيار الحالي، ولا نعرف هل هو ترتيب إصدار سابق من المعيار أم خطأ في الأداة، لأن الأداة أقدم من الإصدار 1.2\. وفي تشغيل آخر للتوقيع على نفس الفاتورة خرج الحقل 9 بطول 33 بايتاً يبدأ بالبايت `00`، وهو سلوك معروف في Java عند تحويل عدد كبير موجب إلى بايتات، وخرج التوقيع مختلفاً عن الأول، وهذا طبيعي في ECDSA لأنه يستخدم رقماً عشوائياً في كل توقيع، فلا تتوقع أن يعطيك توقيع نفس الفاتورة مرتين نفس الرمز. ولاحظ أيضاً أن شهادة الاختبار انتهت في 18 أبريل 2024، ومع ذلك مرت طبقة SIGNATURE في تحققنا، أي أن انتهاء الشهادة لم يمنع نجاح هذه الطبقة في هذه الأداة، أما المنصة فتتحقق من الشهادات بحسب المعيار.

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

والصورة التالية هي الرمز نفسه كما رسمته مكتبة qrcode من النص الذي كتبته الأداة:

![رمز QR مربع بالأبيض والأسود من الإصدار 15 بـ 77 في 77 مربعاً، يحمل 396 حرفاً بترميز base64 للحقول التسعة للفاتورة المبسطة التجريبية](https://arabroot.io/content/images/2026/10/zatca-einvoicing-06-qr.webp)

الإصدار 15 من QR بـ 77 مربعاً في كل ضلع لـ 396 حرفاً

```text
QR version 15 | modules 77 x 77 | characters 396
```

ورمز الحقول الخمسة الأولى الذي بنيناه بأيدينا، بـ 124 حرفاً، يخرج بنفس المكتبة والإعداد من الإصدار 8 بـ 49 مربعاً في الضلع، أما هذا فـ 77 مربعاً، وعلى طابعة حرارية بعرض 58 مليمتراً يصبح كل مربع صغيراً جداً. لذلك اطبع رمزاً تجريبياً على طابعة كل فرع وامسحه بتطبيق الهيئة وبعدة جوالات قبل أن تنشر النظام، لأن الرمز الذي لا يقرأ في الإيصال هو مشكلة لا تظهر في أي تحقق آلي.

## ماذا لو؟ أخطاء نرتكبها عن قصد

### الطول بعدد الحروف

أكثر خطأ يقع فيه من يكتب TLV لأول مرة هو `len(name)` بدل `len(name.encode())`، فالنتيجة واحدة مع الأسماء الإنجليزية، ولا يظهر الخطأ إلا مع اسم عربي. فكتبنا الحقل الأول بطول 20، أي عدد الحروف، ثم قرأناه كما يقرؤه أي قارئ:

```text
== a. TLV length counted in characters instead of UTF-8 bytes
  byte 0: tag 1, length 20: 'مقهى المثا�'
  byte 22: tag 132, length 32: 'التجريبي\x02\x0f39999999990000'
  byte 56: tag 51, length None: runs past the end of the data (57 bytes)
```

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

### تعديل المبلغ بعد التوقيع

ثم غيرنا في الفاتورة الموقعة المبلغ المستحق `PayableAmount` من 86.25 إلى 8.63، كأن أحداً عدل الملف بعد إصداره، وتحققنا:

```text
== c. PayableAmount changed after signing
[INFO] ValidationProcessorImpl - [XSD] validation result : PASSED
[INFO] ValidationProcessorImpl - [EN] validation result : PASSED
[INFO] ValidationProcessorImpl - [KSA] validation result : PASSED
[INFO] ValidationProcessorImpl - [QR] validation result : FAILED
[ERROR] ValidationProcessorImpl - qr validation errors :
[ERROR] ValidationProcessorImpl - CODE : hashedXml, MESSAGE : hashedXml does not match with qr code hashedXml
```

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

### سلسلة الهاش PIH

الحقل PIH في كل فاتورة يحمل هاش الفاتورة التي قبلها، فتتكون سلسلة تبينها الصورة التالية بالهاشات الحقيقية من المختبر:

![سلسلة من اليمين: قبل أول فاتورة PIH يساوي base64 لـ sha256 للنص 0 أي NWZlY2Vi وهي قيمة ثابتة في المواصفة، ثم الفاتورة 1 بالعداد ICV 1 وPIH NWZlY2Vi وهاشها kvihr8CX4oLl، ثم الفاتورة 2 بالعداد ICV 2 وPIH kvihr8CX4oLl وهاشها oZ0yyvl4I8PH، ثم الفاتورة 3 وPIH oZ0yyvl4، وتحتها أن حذف فاتورة من الوسط أو تعديلها يقطع السلسلة بعدها، وأن الفاتورة 2 نجحت مقابل هاش الفاتورة 1 والفاتورة 1 فشلت مقابله برسالة PIH is inValid](https://arabroot.io/content/images/2026/10/zatca-einvoicing-04-chain.webp)

كل فاتورة تشهد على التي قبلها

وبنينا الفاتورة الثانية بعداد 2 وبهاش الأولى في PIH، والأداة تقارن PIH بالقيمة في الملف المحدد في الإعداد `pihPath`، فوضعنا فيه هاش الأولى، ثم تحققنا من الثانية، ثم من الأولى مقابل نفس الملف:

```text
== d. the second invoice, chained to the first (pihPath = /work/pih.txt)
[INFO] InvoiceSigningService -  *** INVOICE HASH = oZ0yyvl4I8PHBGguHAnO1a8mCQ0IrWYdnMQXIatSoeo=
[INFO] ValidationProcessorImpl - [PIH] validation result : PASSED
[INFO] InvoiceValidationService -  *** GLOBAL VALIDATION RESULT = PASSED
== e. the first invoice validated against the same pih.txt
[INFO] ValidationProcessorImpl - [PIH] validation result : FAILED
[ERROR] ValidationProcessorImpl - CODE : KSA-13, MESSAGE : PIH is inValid
[INFO] InvoiceValidationService -  *** GLOBAL VALIDATION RESULT = FAILED
```

والسلسلة هي ما يجعل حذف فاتورة صامتاً أمراً مستحيلاً: لو أصدر المقهى الفواتير 1 و2 و3 ثم حذف 2 من سجلاته، فالفاتورة 3 تحمل هاش 2، والهيئة تستلم 3 دون أن تجد 2، والعداد قفز من 1 إلى 3\. ولهذا السبب نفسه يجب أن يكون لكل جهاز يصدر الفواتير، أي لكل وحدة EGS، عداده وسلسلته، وأن يحفظ آخر هاش في مكان لا يضيع إذا أعيد تشغيل الجهاز. والسؤال الذي واجهه فريق المقهى: إذا كانت في الفرع ثلاثة أجهزة لوحية، هل تكون سلسلة واحدة؟ والإجابة أن السلسلة الواحدة بين ثلاثة أجهزة تحتاج تنسيقاً في كل فاتورة، وأي انقطاع في الشبكة يوقف البيع، ولهذا الأنسب أن يكون كل جهاز وحدة مستقلة بعدادها وسلسلتها وشهادتها، كما يفترض دليل بوابة المطورين حين يتحدث عن وحدات EGS التي تسجل وتختبر كل منها.

![ست طبقات تحقق في أداة SDK وماذا تفحص وما الذي أفشلها في المختبر: XSD لبنية UBL 2.1 ومرت في كل التجارب، وEN 16931 لقواعد الفاتورة الأوروبية ومرت في كل التجارب، وKSA لقواعد المملكة من العنوان الوطني والختم والضريبة وأفشلتها BR-KSA-09 و50 و60 و64 و27، وQR لمطابقة الهاش في الرمز لهاش الملف وأفشلها تعديل المبلغ بعد التوقيع، وSIGNATURE لصحة التوقيع بالمفتاح العام ومرت، وPIH لهاش الفاتورة السابقة وأفشلها التحقق من الفاتورة 1 مقابل سلسلة الفاتورة 2](https://arabroot.io/content/images/2026/10/zatca-einvoicing-05-layers.webp)

كل طبقة تمسك نوعاً مختلفاً من الأخطاء

## التسجيل Onboarding: من أين يأتي مفتاح الختم؟

الختم التشفيري يحتاج مفتاحاً خاصاً وشهادة تصدرها الهيئة، وكل وحدة تصدر فواتير، أي كل وحدة EGS، تمر بتسجيل خاص بها. ويصف دليل بوابة المطورين خطوات هذا التسجيل في بيئة الاختبار: ترسل الوحدة طلب توقيع شهادة Certificate Signing Request واختصاراً CSR فتحصل على معرف ختم للامتثال Compliance CSID، ثم تجتاز فحوص الامتثال بإرسال مستندات اختبار، ثم تطلب معرف ختم الإنتاج Production CSID الذي تختم به الفواتير الحقيقية. وتحدد معايير خصائص الأمان ما يجب أن يحمله طلب الشهادة في حقوله، ومنها:

| الحقل                                   | ما يحمله                                                             | في مثال الأداة              |
| --------------------------------------- | -------------------------------------------------------------------- | --------------------------- |
| الرقم التسلسلي للوحدة EGS Serial Number | بالصيغة 1-اسم المصنع أو المزود\|2-الطراز أو الإصدار|3-الرقم التسلسلي | 1-TST\|2-TST|3-ed22f1d8-... |
| معرف المنشأة Organization Identifier    | الرقم الضريبي من 15 خانة يبدأ بـ 3 وينتهي بـ 3                       | 312345678900003             |
| الوحدة التنظيمية Organization Unit      | اسم الفرع، أو رقم العضو في المجموعة الضريبية                         | 3123456789                  |
| نوع الفواتير Invoice Type               | أربع خانات لـ TSCZ، و1100 تعني فواتير ضريبية ومبسطة                  | 1111                        |

وفي القصة يعني ذلك أن كل جهاز لوحي في كل فرع يولد مفتاحه بنفسه عند تفعيله، ويرسل طلب الشهادة، ويحفظ الشهادة ومعرف الختم، ولا يخرج مفتاحه من الجهاز أبداً. وفي حقل نوع الفواتير يضع المقهى 0100، لأن أجهزته تصدر فواتير مبسطة فقط. ولاحظ أن الرقم الضريبي في مثالنا `399999999900003` يبدأ بـ 3 وينتهي بـ 3 مثل الصيغة المطلوبة، وهو مع ذلك رقم مثال. وتذكر معايير خصائص الأمان في فقرتها الأخيرة أن الواجهات البرمجية للهيئة تستخدم المصادقة الأساسية في OAuth 2، ومعرف العميل Client ID هو الشهادة الصادرة في التسجيل، والسر Secret يصدر معها، ويجب أن يحفظ بأمان ولا يكشف لأحد، فهو في حكم كلمة المرور لهوية الجهاز.

## كيف تلغي فاتورة؟ الإشعار الدائن

في أسبوع التجربة الأول طلب زبون استرداد مبلغ قهوة طلبها بالخطأ، وكان النظام القديم يحذف الفاتورة ويصدر أخرى. وهذا ممنوع الآن كما رأينا، والطريقة الصحيحة إشعار دائن Credit Note، وهو نفس بنية الفاتورة برمز 381 بدل 388، وبنفس الاسم `0200000` لأنه إشعار مبسط. ويطلب معيار XML فيه شيئين إضافيين: القاعدة BR-KSA-56 تجعل مرجع الفاتورة الأصلية في `cac:BillingReference` إلزامياً، والقاعدة BR-KSA-17 تطلب سبب إصدار الإشعار في `cbc:InstructionNote` داخل `cac:PaymentMeans`. والإشعار يأخذ العداد التالي، ويحمل في PIH هاش آخر مستند صدر من الجهاز، أي أنه حلقة في نفس السلسلة، ويبلغ عنه خلال 24 ساعة مثل الفاتورة. والنتيجة أن تاريخ كل بيع يبقى كاملاً: الفاتورة الأصلية، ثم الإشعار الذي ألغاها بسببه، ثم الفاتورة الصحيحة إذا صدرت.

## ما يطلبه الدليل من النظام نفسه

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

- **تصفير العداد**: لا يكون تصفير عداد الفواتير وظيفة متاحة، والوصول إلى قيمته محمي من المستخدمين.
- **تغيير التاريخ**: تغيير تاريخ النظام غير متاح للمستخدمين، والوظائف الممنوعة تذكر إصدار الفواتير بأوقات غير دقيقة.
- **حذف الفواتير أو تعديلها**: لا يحذف أحد الفاتورة بعد إصدارها ولا يعدلها، وإذا أراد إلغاءها فبإشعار دائن ثم فاتورة جديدة.
- **تصدير مفتاح الختم**: المفتاح الخاص يولده الحل بحيث لا يرى ولا ينسخ، لأن تصديره يسمح بسرقة هوية الجهاز، ويقترح الدليل خزنة مفاتيح برمجية أو عتادية Key Vault.
- **الدخول المجهول وكلمة المرور الافتراضية**: كل دخول باسم مستخدم، وكلمة المرور تغير عند أول استخدام، وكل نشاط مسجل في سجلات لا تعدل.

وكل بند من هذه قرار هندسي في نظام المقهى. فالعداد وآخر هاش صارا في جدول لا يقبل إلا الإضافة، والوقت يأخذه النظام من مصدر متزامن لا من الجهاز اللوحي الذي قد يغير موظف تاريخه، ومفتاح الختم لكل جهاز في مخزن المفاتيح الآمن في نظام التشغيل لا في ملف بجانب التطبيق، وزر «حذف الفاتورة» اختفى، وحل محله «إصدار إشعار دائن». وهذه نفس فكرة دفتر القيود الذي لا يحذف منه سطر، التي شرحناها في [المحفظة الرقمية من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D8%AD%D9%81%D8%B8%D8%A9-%D8%B1%D9%82%D9%85%D9%8A%D8%A9-ledger/).

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

## كيف رتب الفريق المسار في النظام؟

بعد المختبر كتب الفريق المسار الذي يمر به كل بيع في المقهى، بالترتيب:

1. **البيع**: يحسب المبلغ والضريبة بأعداد صحيحة بالهللة أو بالنوع Decimal، لا بالكسور العشرية، كما شرحنا في مقال الأرقام العشرية.
2. **بناء XML**: من البيانات مباشرة بمكتبة XML لا بقالب نصي، مع العداد التالي وآخر هاش من جدول الجهاز.
3. **الهاش والختم**: على الجهاز نفسه بمفتاحه، ثم بناء رمز QR بالحقول التسعة.
4. **الحفظ قبل الطباعة**: الملف الموقع يحفظ ويحدث العداد وآخر هاش في معاملة واحدة، ثم تطبع الفاتورة. والسبب أن الطباعة ثم الفشل في الحفظ تعني فاتورة في يد الزبون لا يعرفها النظام، وثغرة في السلسلة.
5. **الإبلاغ**: طابور يرسل الفواتير للمنصة بالترتيب، ويعيد المحاولة، وينبه إذا اقترب عمر أي فاتورة من 24 ساعة.
6. **الرد**: رد المنصة يحفظ مع الفاتورة، والرفض لا يعدل الفاتورة، وإنما يصحح بإشعار وفاتورة جديدة.

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

## اختبار التوافق في خط البناء

أهم ما خرج به الفريق من المختبر ليس الفاتورة نفسها، وإنما طريقة اختبارها. فكل خطأ وجدناه في هذا المقال، من الرقم الإضافي في العنوان إلى المسافات في الهاش إلى الطول بالحروف، لم يكن ليظهر في اختبار يدوي على فاتورة واحدة بأسماء إنجليزية. لذلك أضاف الفريق الأداة نفسها إلى خط البناء CI: Container من Image الـ Java الصغيرة، ومجلد فيه فواتير اختبار يولدها كود النظام نفسه، وسكربت مثل `run_all.sh` في المختبر يوقع كل فاتورة ويتحقق منها، ويفشل البناء إذا ظهر `FAILED` في أي طبقة. والفواتير التي يولدها الاختبار ليست عشوائية، وإنما حالات تتعطل عندها الأنظمة عادة:

- بائع باسم عربي طويل قريب من حد البايت الواحد في الطول، وبنود بأسماء عربية.
- كميات وأسعار تعطي ضرائب بكسور، مثل 36 ريالاً، لأننا رأينا ما تفعله الكسور العشرية بقاعدة واحدة على الأقل.
- فاتورة في الساعة 11:50 مساءً بتوقيت السعودية، لأن تاريخها بتوقيت UTC قد يكون يوماً آخر، فيجب أن يتفق التاريخ في الملف والوقت في الرمز مع ما يراه الزبون.
- إشعار دائن لفاتورة سابقة، بمرجعها وسببه، في وسط السلسلة.
- عشر فواتير متتالية من نفس الجهاز، يتحقق الاختبار أن كل PIH فيها هو هاش التي قبلها وأن العداد لا يقفز.

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

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

## أخطاء شائعة

| الخطأ                           | كيف يظهر                               | البديل                                     |
| ------------------------------- | -------------------------------------- | ------------------------------------------ |
| طول TLV بعدد الحروف             | الاسم العربي يقطع وتختل الحقول بعده    | len(value.encode("utf-8"))                 |
| الوقت المحلي مع Z               | وقت الفاتورة في الرمز متقدم ثلاث ساعات | تحويل الوقت إلى UTC قبل كتابته             |
| إعادة تنسيق XML بعد التوقيع     | الهاش لا يطابق الرمز ولا التوقيع       | الحفظ والإرسال بالبايتات كما وقعت          |
| حذف العناصر دون النص الذي بعدها | هاشك يختلف عن هاش الأداة               | تحويل يبقي النص كما يصفه المعيار           |
| سلسلة واحدة لعدة أجهزة          | تعارض في العداد عند العمل دون اتصال    | عداد وسلسلة وشهادة لكل وحدة                |
| الطباعة قبل الحفظ               | فاتورة لدى الزبون لا يعرفها النظام     | الحفظ والعداد في معاملة ثم الطباعة         |
| المبالغ بالكسور العشرية         | هللات ناقصة أو زائدة في الإجماليات     | Decimal أو أعداد صحيحة بالهللة             |
| مفتاح الختم في ملف              | من ينسخه يصدر فواتير باسمك             | مخزن مفاتيح لا يسمح بالتصدير               |
| الاعتماد على أداة واحدة         | سلوك نسخة قديمة يبنى عليه النظام       | المعيار الحالي ثم بيئة الاختبار في البوابة |

## قائمة تحقق قبل الربط

| البند                                | كيف تتحقق منه                                          |
| ------------------------------------ | ------------------------------------------------------ |
| كل فاتورة تمر بطبقات XSD وEN وKSA    | التحقق في خط البناء CI على فواتير اختبار متنوعة        |
| الهاش تحسبه بنفسك ويطابق الأداة      | اختبار يقارن الهاشين لكل نوع فاتورة                    |
| رمز QR بأسماء عربية طويلة            | فك الرمز وقراءة كل حقل بطوله                           |
| الوقت في الرمز بتوقيت UTC            | فاتورة في منتصف الليل بتوقيت السعودية تظهر بتاريخ صحيح |
| العداد والسلسلة لكل جهاز             | إعادة تشغيل الجهاز لا تغير العداد ولا آخر هاش          |
| لا حذف ولا تعديل للفواتير            | الإلغاء بإشعار دائن فقط                                |
| طابور الإبلاغ يعمل دون اتصال         | تنبيه قبل أن تبلغ أقدم فاتورة 24 ساعة                  |
| المفتاح لا يغادر الجهاز              | لا يوجد مسار في النظام يقرأ المفتاح الخاص              |
| الرمز يقرأ على طابعة كل فرع          | مسح فعلي بتطبيق الهيئة وعدة جوالات                     |
| الاختبار في بيئة البوابة قبل الإنتاج | تسجيل وإرسال فواتير اختبار والردود محفوظة              |

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- الفاتورة الإلكترونية بيانات منظمة لا ملف ممسوح، والمرحلة الثانية منذ 2023 تربط الأنظمة بالهيئة: الفاتورة الضريبية تعتمد قبل التسليم، والمبسطة تختم وتسلم فوراً ثم يبلغ عنها خلال 24 ساعة.
- الفاتورة المبسطة UBL 2.1 بنوع 388 واسم 0200000، وفيها عداد وهاش سابق وعنوان وطني كامل بالرقم الإضافي، وأداة SDK تتحقق منها في ست طبقات.
- في المختبر فشل بند صحيح ضريبته 5.40 في القاعدة BR-KSA-50 لأن القاعدة في هذه النسخة تقارن ناتج 36 في 0.15 بالمساواة التامة، فاقرأ القاعدة قبل أن تغير حسابك.
- الهاش SHA-256 للـ XML الموحد بعد حذف التوقيع والرمز، وحسبناه بأنفسنا فطابق الأداة `kvihr8CX...` بعد أن أصلحنا حذف المسافات، وأي تعديل بعد التوقيع ظهر في طبقة QR.
- رمز QR حقول TLV طولها بالبايتات، والاسم العربي بعشرين حرفاً 38 بايتاً، والرمز بتسعة حقول 396 حرفاً في QR من الإصدار 15، والأداة المنزلة كتبت الحقول 7 و8 و9 بترتيب يختلف عن جدول المعيار الحالي.
- سلسلة PIH تجعل حذف فاتورة ظاهراً، والنظام نفسه يجب أن يمنع تصفير العداد وتغيير الوقت وحذف الفواتير وتصدير المفتاح.

والمقالات المرتبطة:

[![](https://arabroot.io/content/images/2026/10/payment-gateways-cover-cover.webp)دليلبوابات الدفع Payment Gateways في السعودية والبدائل المفتوحةكيف تختار مزود الدفع في المملكة بما تنشره صفحاتهم الرسمية، وأين يقف Stripe والبرمجيات المفتوحة، مع مختبر التحويل بين مزودين.](https://arabroot.io/articles/%D8%A8%D9%88%D8%A7%D8%A8%D8%A7%D8%AA-%D8%A7%D9%84%D8%AF%D9%81%D8%B9-payment-gateways-%D9%81%D9%8A-%D8%A7%D9%84%D8%B3%D8%B9%D9%88%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%A8%D8%AF%D8%A7%D8%A6%D9%84-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1/)[![](https://arabroot.io/content/images/2026/10/fraud-detection-cover-cover.webp)دليلكشف الاحتيال Fraud Detection: قواعد أم تعلم آلة؟قارنا القواعد بنموذجين لتعلم الآلة على بيانات دفع مولدة بنفس سعة المراجعة، ورأينا من يمسك الأنماط المعروفة ومن يتأخر عن النمط الجديد.](https://arabroot.io/articles/%D9%83%D8%B4%D9%81-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D9%84-fraud-detection-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%85-%D8%AA%D8%B9%D9%84%D9%85-%D8%A7%D9%84%D8%A9/)[![](https://arabroot.io/content/images/2026/10/kyc-aml-cover-cover.webp)دليلمعرفة العميل KYC وفحص العقوبات بالأسماء العربيةفحصنا عشرين ألف عميل مولد مقابل قائمة مجلس الأمن، ورأينا كم مدرجاً تفوته المطابقة التامة وكم بريئاً تضيفه كل درجة في العتبة.](https://arabroot.io/articles/%D9%85%D8%B9%D8%B1%D9%81%D8%A9-%D8%A7%D9%84%D8%B9%D9%85%D9%8A%D9%84-kyc-%D9%88%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%B9%D9%82%D9%88%D8%A8%D8%A7%D8%AA-%D8%A8%D8%A7%D9%84%D8%A7%D8%B3%D9%85%D8%A7%D8%A1-%D8%A7%D9%84%D8%B9%D8%B1%D8%A8%D9%8A%D8%A9/)[![](https://arabroot.io/content/images/2026/10/wallet-ledger-cover-cover.webp)دليلالمحفظة الرقمية من الداخل: دفتر القيد المزدوج Ledgerكيف تبني محفظة رقمية لا يضيع فيها ريال ولا يخصم مرتين، بالقيد المزدوج والمعاملات والمطابقة مع مزود الدفع.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%85%D8%AD%D9%81%D8%B8%D8%A9-%D8%B1%D9%82%D9%85%D9%8A%D8%A9-ledger/)[![](https://arabroot.io/content/images/2026/10/qr-payments-cover-cover.webp)دليلالدفع برمز QR: كيف يعمل ومتى يناسبكبنينا رموز دفع بصيغة EMV بلغة Python، وتحققنا منها على أمثلة Pix الرسمية، ورأينا لماذا لا يحمي CRC من استبدال الملصق.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AF%D9%81%D8%B9-%D8%A8%D8%B1%D9%85%D8%B2-qr-%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D9%88%D9%85%D8%AA%D9%89-%D9%8A%D9%86%D8%A7%D8%B3%D8%A8%D9%83/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال وتشغيل المختبر على python:3.13.17-slim (lxml 6.0.2 وcryptography 46.0.3 وqrcode 8.2) وeclipse-temurin:11.0.32\_9-jre-noble مع أداة SDK من صفحة التنزيل في الهيئة (الملف zatca-envoice-sdk-203.zip، الأداة 3.0.8)، ومراجعة معيار XML ومعايير خصائص الأمان (الإصدار 1.2) والإرشادات التفصيلية (الإصدار 2) وقرار الضوابط، وكلها بتاريخ 19 مايو 2023، كما ظهرت في 27 أكتوبر 2026.