عميل في السعودية عنده نظام نقاط بيع (POS) انبنى بمنصة توليد تطبيقات بالذكاء الاصطناعي، ويشتغل لأكثر من متجر من نفس النظام. صفحات النظام تقول «معتمد من زاتكا»، والفاتورة تطلع وعليها رمز QR. لكن لما جا وقت الربط الفعلي مع منصة «فاتورة»، الفواتير ما كانت توصل صح. هذي القصة التقنية كاملة: إيش لقينا، وليش صار، وكيف صلّحناه.
باختصار
| قبل | بعد | |
|---|---|---|
| التوقيع | «ختم» وهمي بمفتاح ثابت مكتوب في الكود | توقيع ECDSA على منحنى secp256k1، داخل الفاتورة بصيغة XAdES |
| شهادة الجهاز (CSID) | ما فيه تسجيل أصلًا | تسجيل كامل: CSR، ثم رمز OTP من «فاتورة»، ثم شهادة الإنتاج |
| بصمة الفاتورة | SHA-256 لنص JSON | SHA-256 لملف UBL بعد توحيد صيغته (C14N) |
| سلسلة الفواتير (PIH) | سلسلة وحدة مشتركة بين كل المتاجر | سلسلة مستقلة لكل متجر، محفوظة في قاعدة البيانات |
| رمز QR | الحقول 1 إلى 5 فقط (المرحلة الأولى) | الحقول 1 إلى 8: البصمة، والتوقيع، والمفتاح العام |
| B2B و B2C | كل فاتورة «إبلاغ» | الفاتورة الضريبية «اعتماد» (Clearance)، والمبسطة «إبلاغ» (Reporting) |
| الأخطاء | الفاتورة اللي تفشل تبقى معلّقة للأبد | 3 محاولات، ثم مراجعة دورية، ولوحة تبيّن حالة كل فاتورة |
| الاختبارات | 0 اختبار للفوترة | 64 اختبار للفوترة الإلكترونية |
النتيجة: الفواتير تنرسل لـ «فاتورة» وتنقبل، وكل متجر له سلسلته وشهادته.
ليش هذا النوع من المشاكل يطلع في التطبيقات المبنية بالذكاء الاصطناعي
أدوات توليد التطبيقات تبني لك اللي تشوفه: شاشة البيع، والفاتورة، ورمز QR. والمرحلة الثانية من الفوترة الإلكترونية أغلبها شي ما ينشاف: توقيع رقمي، وسلسلة بصمات، وشهادة صادرة من الهيئة لكل جهاز. الأداة تقدر تطلّع رمز QR «شكله صح» وفاتورة «شكلها صح»، وتكتب في الصفحة «معتمد»، بدون ما يكون فيه أي ربط حقيقي. وهذا اللي لقيناه.
إيش لقينا
1. التوقيع كان مفتاح ثابت مكتوب في الكود
بدل التوقيع بالمفتاح الخاص للشهادة اللي تصدرها الهيئة، الكود كان يحسب HMAC بمفتاح نصّي ثابت، والتعليق نفسه يقول إنه «محاكاة»:
def generate_cryptographic_stamp(self, invoice_hash: str) -> str:
"""... Here we simulate with HMAC-SHA256."""
# In production: sign with ZATCA-issued certificate private key
stamp_key = b"ZATCA_PRODUCTION_SIGNING_KEY_PLACEHOLDER"
signature = hmac.new(stamp_key, invoice_hash.encode(), hashlib.sha256).hexdigest()
return base64.b64encode(signature.encode()).decode()أي فاتورة بهذا «الختم» ترفضها «فاتورة»، لأن الهيئة تتحقق من التوقيع بالمفتاح العام اللي في شهادة الجهاز، وهنا ما فيه شهادة ولا مفتاح.
2. ما فيه تسجيل للجهاز (Onboarding)
المرحلة الثانية تطلب إن كل جهاز يصدر فواتير (EGS) يسوّي مفتاح خاص، ويرسل طلب شهادة (CSR) للهيئة، ويتحقق برمز OTP من بوابة «فاتورة»، ويستلم شهادة التوافق ثم شهادة الإنتاج. في النظام ما كان فيه أي خطوة من هذي: جدول المتجر فيه رقم ضريبي وعدّاد فواتير وبس.
3. ملف الفاتورة (UBL) موجود، لكن ما أحد يستخدمه
كان فيه كود يبني ملف XML بصيغة UBL، لكنه موصول بنقطة وصول يدوية وحدة، يرجّع الملف بدون توقيع وبدون ما يحفظه. البيع الفعلي من الكاشير ما كان يمر عليه أبدًا.
4. «بصمة الفاتورة» كانت بصمة لنص JSON
invoice_json = json.dumps(invoice_data, sort_keys=True)
invoice_hash = hashlib.sha256(invoice_json.encode()).hexdigest()الهيئة تحسب البصمة من ملف الفاتورة نفسه بعد توحيد صيغته (Canonicalization)، وتكون بترميز base64. بصمة لنص JSON بترميز hex ما تطابق أي شي تحسبه «فاتورة».
5. سلسلة الفواتير مشتركة بين كل المتاجر
كل فاتورة لازم تحمل بصمة الفاتورة اللي قبلها من نفس الجهاز (PIH)، فتصير الفواتير سلسلة ما ينفع تنحذف منها حلقة. الكود كان يحفظ «آخر بصمة» في متغيّر واحد داخل الخادم:
self._previous_hash: str = "0" * 64 # Initial hash for chain
...
embedded.text = self._previous_hash
...
self._previous_hash = invoice_hashيعني فاتورة متجر (أ) تحمل بصمة فاتورة متجر (ب)، والسلسلة تنكسر مع كل إعادة تشغيل للخادم. وحتى لما انضاف حقل للسلسلة لكل متجر في أول جولة إصلاح، الملف كان لسا يكتب القيمة من المتغير المشترك. هذا لقيناه في مراجعة ثانية وصلّحناه.
6. رمز QR من المرحلة الأولى
الرمز كان فيه 5 حقول: اسم البائع، والرقم الضريبي، والوقت، والإجمالي، والضريبة. المرحلة الثانية تضيف بصمة الفاتورة، والتوقيع، والمفتاح العام. والطول كان يتكتب بايت واحد، فأي حقل أطول من 255 بايت يخرب الرمز كله.
7. كل فاتورة تنرسل «إبلاغ»
الفاتورة الضريبية بين منشأتين (B2B) لازم تتعتمد من الهيئة قبل ما تنرسل للمشتري (Clearance)، والفاتورة المبسطة للأفراد تنبلّغ خلال 24 ساعة (Reporting). النظام ما كان يفرّق، وما كان يحفظ الرقم الضريبي للمشتري أصلًا.
كيف صلّحناه
التوقيع الحقيقي
def sign_invoice_hash(private_key_pem: str, invoice_hash_base64: str) -> str:
key = serialization.load_pem_private_key(private_key_pem.encode("utf-8"), password=None)
digest = base64.b64decode(invoice_hash_base64)
signature = key.sign(digest, ec.ECDSA(utils.Prehashed(hashes.SHA256())))
return base64.b64encode(signature).decode("ascii")المفتاح الخاص لكل متجر ينحفظ مشفّر (AES-256-GCM)، وما ينكتب في الكود ولا في السجلات.
بصمة من الملف الموحّد، وتوقيع داخل الفاتورة
# Hash over the C14N invoice so Fatoora's recomputed digest matches ours.
inv_hash = zatca.invoice_hash_b64(zatca.canonicalize_invoice_xml(xml))
key_pem = zatca.decrypt_secret(creds.private_key_enc)
stamp = zatca.sign_invoice_hash(key_pem, inv_hash)
signed_xml = zatca.build_signed_xml(
unsigned_xml=xml, private_key_pem=key_pem, invoice_hash_b64=inv_hash,
signing_time=signing_time,
certificate_b64=getattr(creds, "production_csid", None) or None,
)التوقيع ينحط داخل الفاتورة نفسها في UBLExtensions، مع الشهادة ووقت التوقيع، مثل ما تطلبه الهيئة.
سلسلة لكل متجر، ورقم فاتورة واحد من البداية للنهاية
# The PIH is this client's recorded chain head: embedded into the UBL AND
# recorded on the document. It must never bleed across tenants.
previous_hash = creds.pih or zatca.INITIAL_PIH
# One UUID, persisted and embedded: the cbc:UUID, the stored document
# and the value submitted to Fatoora must all be this same string.
invoice_uuid = str(uuid.uuid4())كل بيع الحين يمر على خطوة وحدة: يبني ملف UBL، ويحسب البصمة، ويوقّع، ويحفظ الفاتورة الموقّعة وحلقة السلسلة في نفس العملية في قاعدة البيانات. إذا فشل شي، ما ينحفظ نص فاتورة.
رمز QR بالحقول الثمانية
payload += _tlv_field(5, vat_amount.encode("utf-8"))
payload += _tlv_field(6, base64.b64decode(invoice_hash_b64))
payload += _tlv_field(7, base64.b64decode(stamp_b64))
payload += _tlv_field(8, base64.b64decode(public_key_der_base64))
الاعتماد والإبلاغ، والمحاولة من جديد
- الفاتورة لمنشأة لها رقم ضريبي، وقيمتها 1,000 ريال أو أكثر، تنرسل «اعتماد» (
/invoices/clearance/single) قبل ما توصل المشتري، والباقي فواتير مبسطة تنرسل «إبلاغ» (/invoices/reporting/single). - إذا «فاتورة» ما ردّت، أو ردّت بخطأ من الخادم (5xx)، النظام يحاول 3 مرات. وإذا رفضت الفاتورة نفسها (4xx)، تتعلّم «مرفوضة» مع سبب الرفض، وتطلع للمتجر.
- مراجعة دورية تلقى أي فاتورة معلّقة وترسلها، ولوحة تبيّن كل فاتورة وحالتها.
- الإرسال الحقيقي ما يشتغل إلا بعد ما يكتمل تسجيل المتجر وتنصدر شهادة الإنتاج. قبلها الفاتورة توقَّع وتنحفظ وتنتظر.
فحص السلسلة من شاشة المتجر

كيف تأكدنا
- 64 اختبار للفوترة الإلكترونية بدل صفر، منها اختبار يتأكد إن سلسلة متجر ما تتسرّب لمتجر ثاني. كتبنا هذا الاختبار قبل الإصلاح، وفشل على الكود القديم، ثم نجح.
- كل خطأ لقيناه انكتب له اختبار يفشل أول، ثم الإصلاح.
- بعد التسجيل وإصدار شهادة الإنتاج، الفواتير انرسلت لـ «فاتورة» وانقبلت.
المدة
12 يوم للإصلاح الأساسي، من أول مراجعة لين التوقيع والسلسلة والتسجيل. وبعدها أسبوعين لتفاصيل الضريبة: فئة الضريبة لكل سطر (خاضع، صفري، معفى) بدل 15% ثابتة، وحفظ الفواتير 6 سنين مع إثبات إنها ما تغيّرت.
إذا عندك نظام فواتير انبنى بالذكاء الاصطناعي
قبل ما تعتمد على كلمة «معتمد من زاتكا» في أي نظام، اسأل هذي الأسئلة:
- وين المفتاح الخاص للتوقيع؟ إذا الجواب «في الكود» أو «ما أدري»، فيه مشكلة.
- فيه خطوة تسجيل برمز OTP من بوابة «فاتورة»؟ بدونها ما فيه شهادة، وبدون شهادة ما فيه ربط.
- افتح ملف XML لأي فاتورة: فيه
ds:Signature؟ وفيه بصمة الفاتورة اللي قبلها؟ - امسح رمز QR بتطبيق الهيئة: كم حقل يطلع؟
- عندك أكثر من فرع أو متجر؟ كل واحد له سلسلته؟
تفاصيل المراحل والمواعيد في الفوترة الإلكترونية للمنتجات السعودية. وإذا تبي أحد يراجع نظامك، تدقيق جاهزية الإطلاق يمر على هذا كله، وإذا عرفت المشكلة وتبيها تنصلح، الإصلاح المباشر.