مكتبة الفاتورة الإلكترونية المصرية (ETA)

طبقة برمجية للتعامل مع منظومة الفاتورة الإلكترونية المصرية — بمعايير مُتحقَّقة من عيّنة الهيئة الرسمية، لا من الاجتهاد.


لماذا

القياس الرقم
حزم npm لمنظومة ETA صفر
مستودعات ETA على GitHub مقابل ZATCA 5 مقابل 627
المرجع الرسمي EInvoicingSigner مهجور منذ 30 يناير 2024 — 26 مشكلة مفتوحة، 63 نسخة مشتقة

63 نسخة مشتقة من مرجع متروك تعني 63 فريقاً لم يجدوا بديلاً.

والوضع النظامي مفتوح لهذا بالذات: المصلحة تخاطب «مطوري الممولين» نصاً في أدلتها، وترخيص «مقدم الخدمة» يخصّ طبقة الوساطة لا مورّدي البرمجيات. التفصيل في research/ETA-LEGAL-CLEARANCE.md.


ما هو مبنيّ

① التسلسل الكنسي والتجزئة — serialization.py

أصعب قطعة في التكامل. مُتحقَّق منه بايتاً ببايت مقابل الملف الرسمي one-doc-serialized.json.txt المنشور على بوابة الهيئة.

from eta_invoice.serialization import load_document, canonical_hash

doc = load_document(Path("invoice.json").read_text(encoding="utf-8"))
print(canonical_hash(doc))   # D43FAE25B02B7E466FA5AA39E5FA45C7D92661E5B0A0E7964E04783BFF3C2651

الفخّ الذي تقع فيه أغلب النسخ: المواصفة تُلزم بأخذ القيمة «بلا أي معالجة» — فـ0.0 تبقى 0.0 ولا تصير 0 ولا 0.00. وjson.loads العادي يحوّلها إلى float فيضيع شكلها اللفظي، فيختلف الهاش عن هاش الهيئة ويُرفض المستند بلا تشخيص مفيد. الحل هنا: parse_float=str وparse_int=str.

①ب التسلسل الكنسي لصيغة XMLserialization_xml.py

كثير من أنظمة ERP ترسل XML لا JSON، والمواصفة تعرّف لهما خوارزميتين مختلفتين. ومن طبّق خوارزمية JSON على XML حصل على هاش صالح شكلاً خاطئ فعلاً — فرُفض مستنده بلا تشخيص.

  JSON XML
اسم الجذر لا يُدرَج يُدرَج — يبدأ الناتج بـ"DOCUMENT"
المصفوفات الاسم يُكرَّر قبل كل عنصر لا معالجة خاصة — كل وسم يحمل اسمه
الاقتباس لا تهريب "\" — حمايةٌ من تصادم الهاش

مُتحقَّق بايتاً ببايت من one-doc-serialized.xml.txt الرسمي (3,917 حرفاً).

⚠️ وفخّ موثّق: المسافات بين الوسوم لا أثر لها، والمسافات داخل وسم ورقي لها أثر — لأن القيمة تُؤخذ بلا معالجة. فمن نسّق مستنده بأداة تُضيف مسافات داخل الأوراق، تغيّر هاشه.

② المُحقِّق المحلي — validation.py

يمنع الرفض قبل الإرسال، ويشرح السبب.

from eta_invoice.validation import validate_document, format_report
print(format_report(validate_document(doc)))
[خطأ] invoiceLines[0].salesTotal: إجمالي البيع لا يطابق المحسوب من مكوّناته
    المتوقَّع: 947.0    الموجود: 950.0
    الإصلاح: salesTotal = quantity × unitValue.amountEGP (الفارق 3.0)

ثماني قواعد، كلها مشتقّة من العيّنة الرسمية ومُختبَرة عليها:

# القاعدة
1 salesTotal = quantity × unitValue.amountEGP
2 netTotal = salesTotal − discount.amount
3 totalSalesAmount = Σ salesTotal
4 totalDiscountAmount = Σ discount.amount
5 netAmount = totalSalesAmount − totalDiscountAmount
6 totalItemsDiscountAmount = Σ itemsDiscount
7 taxTotals[type] = Σ taxableItems لكل نوع
8 totalAmount = Σ line.total − extraDiscountAmount

③ بنّاء المستند — builder.py

المُحقِّق يمسك الخطأ بعد وقوعه؛ والبنّاء يمنع وقوعه: يحسب كل حقل مشتقّ بـDecimal، ويجمّع كل إجماليات المستند، ويفحص ناتجه بنفسه قبل التسليم.

b = InvoiceBuilder(issuer=..., receiver=..., internal_id="INV-1", activity_code="6201")
b.add_line(InvoiceLine(description="خدمة", quantity=1,
                       unit_price_egp="12000.00", total="13680.00",
                       taxes=[Tax("T1", rate=14)]))
doc = b.build()          # يرمي BuildError لو أخفق فحصه لنفسه

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

ضمانة مُختبَرة: 200 فاتورة عشوائية بمولّد حتمي — كلها تمرّ بالمُحقِّق.

④ كتابة JSON متطابقة مع التجزئة — dump_document

⚠️ الهاش مبنيّ على الشكل النصّي للقيمة. فكاتب JSON يكتب 114 حيث نجزّئ 114.0 — وكلاهما JSON صحيح — يُغيّر الهاش فيُرفض المستند بلا تشخيص. فاكتب مستندك بـdump_document، أو اضمن أن كاتبك يطابق تمثيلنا.


⑤ التوقيع الإلكتروني — signing.py

منقولٌ عن نصّ المواصفة الرسمية لا عن مثالٍ ولا عن مكتبة: ITIDA — Digital Signature Format for E-Invoice System، الإصدار 1.1، 10 نوفمبر

  1. ستّ صفحات، نُزِّلت واستُخرج نصّها.

⚠️ الفخّ الذي يُسقط كل من استعمل مكتبة CMS بإعداداتها: encapContentInfo.eContentType ليس id-data المعتاد (1.2.840.113549.1.7.1) بل id-digestedData (1.2.840.113549.1.7.5)، وكذلك سمة content-type الموقَّعة. وكل مكتبة CMS تختار id-data تلقائياً — فيخرج توقيعٌ صحيحٌ تشفيرياً ترفضه وحدةُ تحقّق ITIDA. وهو أسوأ إخفاق ممكن: يمرّ كلَّ فحصٍ عام، ويسقط في الفحص الوحيد الذي يهمّ.

وفخٌّ ثانٍ: التوقيع يقع على ترميز SET OF للسمات (0x31) لا على الوسم الضمني [0] (0xA0) الذي تُحمل به في الرسالة — RFC 5652 §5.4. وقد أوقعني بنفسي في أول تشغيل، فأُثبت الفرقُ اختباراً.

from eta_invoice import SignerLike, build_cades_bes, inspect_cades_bes

signer = SignerLike(certificate_der, sign=my_token.sign)   # لا يمرّ مفتاحٌ خاص
signature = build_cades_bes(canonical_bytes, signer)

report = inspect_cades_bes(signature, canonical_bytes)
report.conformant     # True
report.problems       # []

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


ما ليس مبنيّاً — وأقوله صراحةً

🔴 قاعدة غير محسومة: invoiceLines[].total

جُرّبت netTotal + valueDifference + totalTaxableFees ± الضرائب − itemsDiscount على العيّنة الرسمية فأعطت فرقاً مقداره 912.00 في البند الأول، ونصف قرش غير قابل للتمثيل في البند الثاني — أي أن الخلل في الصيغة لا في قطبية أنواع الضرائب.

ولذلك لا تُفحص هذه القاعدة. مُحقِّقٌ يرفض فاتورة صحيحة أسوأ من مُحقِّق يفحص أقل: الأول يُفقد الثقة فيُهمَل، فلا يمنع رفضاً أبداً. مسجَّلة في validation.UNRESOLVED وفي اختبار يفرض ألا تُطبَّق.

🔴 حدُّ التوقيع: مطابقٌ للمواصفة، ولم يُشغَّل على وحدة التحقّق

المصلحة لا تمنح حساب بيئة تجريبية للمطوّر المستقل: الدخول «كمستخدم بصفة مدير مسئول لدى الممول». والختم الإلكتروني «لا يرتبط إلا بكيان قانوني».

فالتوقيع في signing.py مطابقٌ لنصّ المواصفة ومُتحقَّقٌ من كل حقلٍ فيه بالتفكيك، ويصحّ تشفيرياً — ولم يُشغَّل بعدُ على وحدة تحقّق ITIDA ولا على شهادة ختمٍ حقيقية. ولا يُدَّعى قبولٌ لم يُرَ.

🔴 الإرسال — لا يُبنى قبل قراءة الشروط

لا يُكتب سطرٌ في إرسال المستندات قبل قراءة شروط البوابة من داخل حسابٍ مسجَّل. حاجزٌ صلب، واختبارٌ في test_serialization.py يفرض ألا يتسرّب إرسالٌ غير مبنيّ إلى السطح العام.


التشغيل

python -m pytest eta-lib/tests -q

68 اختباراً، منها المطابقة الحرفية للعيّنتين الرسميتين (JSON وXML) و200 فاتورة عشوائية.

المصادر

نبني تكاملات فوترة إلكترونية ونصلح المكسور منها — بنطاقٍ وسعرٍ واضحين. راسلنا أو انسخ العنوان: yahya@opus-studio.pro