فاتورة — فاتورة ضريبية عربية بصيغة PDF
نصٌّ مرتَّب صحيحاً، ومجاميع بالهللات، ورمز QR مبنيّ على نصّ مواصفة الهيئة — في ملف واحد جاهز للطباعة أو الإرسال.
import { renderInvoice } from "fatura-zatca";
const { pdf, totals, qrPayload } = await renderInvoice({
number: "INV-2026-0042",
issuedAt: "2026-09-01T13:45:00+03:00",
currency: "SAR",
seller: { name: "مؤسسة الأمل التجارية", vatNumber: "310122393500003" },
lines: [
{ description: "استشارة برمجية", quantity: 2, unitPrice: 50_000, vatRate: 15 },
],
}, { fontBytes, amountInWords: tafgeet });
المبالغ أعدادٌ صحيحة بأصغر وحدة: 50_000 تعني ٥٠٠٫٠٠ ريالاً.
ثلاثة قرارات تُميّز هذه الحزمة
① الحساب بالهللات — لا بالكسور العشرية
0.1 + 0.2 !== 0.3 في كل لغةٍ تستعمل الفاصلة العائمة، و(1.005).toFixed(2)
تُعطي "1.00" والصواب "1.01". فاتورةٌ تُبنى على ذلك تُخطئ في هللة — وتلك
الهللة هي ما يرفض المُحقِّق الفاتورةَ من أجله.
فكل المبالغ أعدادٌ صحيحة، والقسمةُ على مئة آخرُ عملية لا أولها.
والضريبة تُحسب لكل بند ثم تُجمع، لا على المجموع. الفرق هللةٌ أو اثنتان في فاتورةٍ ذات بنود كثيرة، وهي التي تُفشل المطابقة.
② لا نخترع مالاً
lineTotal يُؤخذ من نظامك المحاسبي إن مرّرته — فالتقريب في ضرب كميةٍ كسرية
قرارٌ محاسبي يخصّك. وإن غاب حُسب quantity × unitPrice بتقريب نصفٍ إلى أعلى،
وقيل ذلك صراحةً.
والمبلغ بالحروف يُمرَّر من محرّك تفقيط تختاره؛ وإن لم تمرّره لم يُطبع السطر — ولا تُخترع صياغةٌ نحوية بلا محرّك.
③ رمز QR من نصّ المواصفة لا من عيّنة
ZATCA — Electronic Invoice Security Features Implementation Standards v1.1، §4.1 والجدول 3.
وسببُ التشديد قصّة: ذهبنا نتحقق من مولّدنا بعيّنة Base64 «رسمية» مستعادة من الذاكرة، فلم تُطابق. وقبل تعديل الكود فُكِّكت العيّنةُ نفسها فإذا هي فاسدة — تُعلن الوسم 4 بطول 6 ثم لا ينتظم ما بعدها TLV. المرجع كان خاطئاً والتطبيق سليماً، ولو صُدِّقت الذاكرة لصار الصوابُ خطأً.
فالاختبار اليوم يبني المتوقَّع بمشفّرٍ مستقل مكتوب من نصّ §4.1، ويقارنه بنسخة بايثون المرجعية أيضاً — فمقارنة الكود بنفسه لا تُثبت شيئاً.
والفخّ الذي يُسقط كل تطبيق ساذج: الطول بالبايتات لا بالأحرف. «شركة» أربعة أحرف وثمان بايتات، ومن عدّ الأحرف أنتج رمزاً يفشل قارئه بلا رسالة.
الاختبارات تقرأ الملف، لا مخرَج الدالة
٣٢ اختباراً، وأهمّها ما يفتح ملف الـPDF المُنتَج ويقرأ منه:
- معرّفات الرسوم تُردّ إلى محارفها عبر خريطة الخط، فيُتحقَّق أن
1,546.75طُبعت كما تُقرأ لا57.645,1 - ورمز QR يُعاد بناؤه من المربّعات المرسومة فعلاً على الصفحة، ثم يُفكّ
بـ
jsqr— قارئٌ مستقلّ لم نكتبه — ويُطابَق بمجاميع الفاتورة
وهذا هو الفرق العملي كله: حزمٌ منشورة تُخرج نصّاً صحيحاً من دوالّها ثم تُنتج فاتورةً خاطئة، لأن مكتبة الرسم تعكس المقاطع بعدها. فاختبارٌ يفحص القيمة المُعادة يمرّ، والعميل يتسلّم مبلغاً مقلوباً.
npm test
node examples/sample.mjs # فاتورة كاملة للنظر إليها
وما كشفته العينُ ولم يكشفه الاختبار
كانت الاختبارات كلها خضراء، والفاتورة المطبوعة تحمل:
(15% )150.00 ← والصواب 150.00 (15%)
(15)% ← والصواب (15%)
SAR 1,571.75 ← والصواب 1,571.75 SAR
ثلاثتها ترتيبٌ صحيح بقواعد يونيكود لنصٍّ لم يُعزل: الأقواس وعلامة النسبة محايدة، تأخذ اتجاه ما حولها فتتفرّق على جانبَي الرقم. والخطأ خطؤنا — تركنا الجارَ يحكم على ما ليس منه.
والعلاج أداةُ يونيكود لهذا الغرض بعينه: العزل الاتجاهي. وهو الآن في
nasq بدالّة isolate()، وتستعمله هذه الحزمة في كل خليةٍ
رقمية. وصارت الثلاثة اختباراتِ انحدارٍ حتى لا تعود.
والدرس مُدوَّن: لا تكفي البوّابةُ الخضراء. من لم ينظر إلى صفحته لا يستطيع أن يشهد لها.
ما ليس هنا
- الوسوم 6-9 (هاش XML، توقيع ECDSA، المفتاح العام، توقيع الهيئة للفواتير المبسطة) تخصّ المرحلة الثانية وتلزم من ربطته الهيئة بمنصة «فاتورة». ولا تُبنى بلا شهادة ختمٍ حقيقية، ولن نشحن كوداً لم يُشغَّل عليها.
- الخطّ — يمرّره المستدعي. للخطوط رخصها، وحزمةٌ تحمل خطاً بلا داعٍ تُثقل من لا يحتاجه.
- حكمُ قبولٍ من الهيئة. هذه الحزمة تبني ملفاً؛ وحالة القبول تعيش في استجابة المنصة، ولا تملكها أداةٌ محلية.
القطع
nasq |
ترتيب النصّ العربي — bidi مطابق لـ٩١٬٧٠٧ حالة من يونيكود |
eta-lib |
التسلسل الكنسي والتجزئة لمنظومة مصر |
| مُتوافِق | التفقيط — ١٠٠٪ على مقياس التفقيط العربي |
الرخصة: MIT · جزء من منظومة الفوترة العربية
yahya@opus-studio.pro