zatca-qr
v1.0.2
Published
ZATCA (Saudi FATOORA) e-invoice QR: encode, decode and validate in TLV + Base64. Byte-accurate Arabic text, zero dependencies. مولّد وقارئ رمز فاتورة زاتكا.
Maintainers
Readme
zatca-qr
تولّد رمز QR لفواتير زاتكا السعودية وتقرأه. بدون مكتبات خارجية وبدون خادم.
المشكلة
في السعودية كل فاتورة ضريبية لازم يكون عليها رمز QR. الرمز مكتوب بصيغة اسمها TLV وبعدها Base64. إذا كان الرمز غلط، نظام المشتري يرفض الفاتورة.
الخطأ المشهور هو حساب طول الحقل بعدد الحروف. لكن TLV يحسب الطول بعدد البايتات. الحرف العربي = بايتين. لذلك:
| اسم البائع | عدد الحروف | عدد البايتات | النتيجة |
| --- | --- | --- | --- |
| Acme Saudi | 10 | 10 | ✅ سليم |
| شركة النخبة | 10 | 20 | ❌ الرمز يُرفض |
| ش × 128 | 128 | 256 | ❌ أكبر من حد TLV (255) |
هذه المكتبة تحسب البايتات صح، وتقرأ الرمز لو وصلك من جهة ثانية، وتفحص الفاتورة قبل ما ترسلها.
التثبيت
npm install zatca-qr
# أو مباشرة من المستودع
npm install github:exeerkit/zatca-qrبدون أي مكتبة خارجية. تعمل في Node 18 وأحدث، وفي المتصفح، وفي Cloudflare Workers وDeno وBun.
الاستخدام
import { encodeZatcaTlv, toZatcaAmount, validateZatcaInvoice } from 'zatca-qr'
const invoice = {
sellerName: 'مؤسسة النخبة التجارية',
vatNumber: '300000000000003', // 15 رقماً، يبدأ بـ3 وينتهي بـ3
timestamp: new Date(), // أو نص ISO 8601 مع منطقة زمنية
totalWithVat: toZatcaAmount(1150), // يطلع '1150.00'
vatTotal: toZatcaAmount(150), // يطلع '150.00'
}
// 1) افحص الفاتورة قبل الإرسال
const report = validateZatcaInvoice(invoice)
if (!report.valid) throw new Error(report.errors[0].message)
// 2) خذ نص الرمز
const payload = encodeZatcaTlv(invoice, { strict: true })
// 3) حطه في أي مولد QRمع React
import { QRCodeSVG } from 'qrcode.react'
import { encodeZatcaTlv, type ZatcaInvoiceData } from 'zatca-qr'
export function InvoiceQr({ invoice }: { invoice: ZatcaInvoiceData }) {
return <QRCodeSVG value={encodeZatcaTlv(invoice)} size={220} level="M" />
}على Cloudflare Workers
import { encodeZatcaTlv } from 'zatca-qr'
export default {
fetch(request: Request) {
const url = new URL(request.url)
const payload = encodeZatcaTlv({
sellerName: url.searchParams.get('seller') ?? '',
vatNumber: url.searchParams.get('vat') ?? '',
timestamp: new Date(),
totalWithVat: url.searchParams.get('total') ?? '0.00',
vatTotal: url.searchParams.get('vat_amount') ?? '0.00',
})
return Response.json({ qrPayloadBase64: payload })
},
}اقرأ رمز وصل إليك
تنفع لما تفحص رمز من نظام ثاني، أو تكتشف رمز مكتوب يدوي غلط.
import { decodeZatcaTlv } from 'zatca-qr'
const decoded = decodeZatcaTlv(payloadFromScanner)
console.log(decoded.phase) // 1 أو 2
console.log(decoded.data.sellerName) // 'مؤسسة النخبة التجارية'
console.log(decoded.fields) // كل حقل مع طوله بالبايتات
console.log(decoded.warnings) // حقل مكرر؟ حقل ناقص؟ حقول غريبة؟تقرأ المكتبة أيضاً Base64 الآمن للروابط، وترد الرموز الغريبة بدون ما تكسر الباقي.
جدول الحقول
| الرقم | الحقل | النوع | المرحلة |
| --- | --- | --- | --- |
| 1 | اسم البائع | نص UTF-8 | الأولى (مطلوب) |
| 2 | الرقم الضريبي | 15 رقماً يبدأ وينتهي بـ3 | الأولى (مطلوب) |
| 3 | وقت الفاتورة | ISO 8601 مع منطقة زمنية | الأولى (مطلوب) |
| 4 | الإجمالي مع الضريبة | رقم بفاصلة نقطية | الأولى (مطلوب) |
| 5 | مبلغ الضريبة | رقم بفاصلة نقطية | الأولى (مطلوب) |
| 6 | بصمة ملف الفاتورة | Base64 (SHA-256) | الثانية (اختياري) |
| 7 | التوقيع الرقمي | Base64 (ECDSA) | الثانية (اختياري) |
| 8 | المفتاح العام | Base64 | الثانية (اختياري) |
| 9 | ختم الهيئة | Base64 | بعد الاعتماد فقط |
المرحلة الثانية: وش تسوي المكتبة وش ما تسوي
بصراحة: المكتبة تغلّف الحمولة وتفكها فقط. ما تحسب لك بصمة الملف، وما توقّع، وما تتصل بهيئة الزكاة. البصمة والتوقيع تجيبهم من شهادتك (CSID) وتعطيها للمكتبة:
const payload = encodeZatcaTlv({
...invoice,
invoiceHash, // بصمة ملف الفاتورة XML
signature, // التوقيع من شهادة CSID
publicKey, // المفتاح العام من نفس الشهادة
// stamp تضيفه بعد اعتماد الهيئة
})قاعدة بسيطة: الحقول 6 و7 و8 تجي مع بعض. وختم الهيئة ما ينقبل بدونهم.
أخطاء ترفض الفاتورة
| الغلط | الصح |
| --- | --- |
| وقت بدون منطقة (2026-04-18 13:30) | 2026-04-18T13:30:00+03:00 أو ...Z |
| فاصلة عربية (1150,00) أو فاصلة آلاف (1,150.00) | 1150.00 |
| مبلغ بدون منزلتين (115) | 115.00 استخدم toZatcaAmount |
| ترتيب الحقول غلط | المكتبة ترتبها صح دايم |
| رقم ضريبي من 14 رقماً | 15 رقماً يبدأ وينتهي بـ3 |
العملة في هذا الحقل ريال سعودي دايم، حتى لو الفاتورة بعملة ثانية في ملف XML.
رموز الأخطاء والتنبيهات
كل خطأ وتنبيه له رمز ثابت في code، فتقدر تتعامل معه برمجياً بدل ما تقارن نص الرسالة.
الأخطاء توقف validateZatcaInvoice وترجع valid: false، والتنبيهات ترجع مع valid: true.
| الرمز | النوع | متى يظهر | وش تسوي |
| --- | --- | --- | --- |
| MISSING_FIELD | خطأ | حقل إلزامي فاضي أو غير موجود | مرّر الحقل الخمسة كاملة |
| INVALID_FIELD | خطأ | نوع غير مدعوم، أو رقم في حقل نصي | مرّر نصاً، والمبالغ فقط تقبل رقماً |
| FIELD_TOO_LONG | خطأ | الحقل أكبر من 255 بايتاً | قصّر النص، وتذكّر الحرف العربي بايتان |
| VAT_NUMBER_FORMAT | خطأ | الرقم الضريبي مو 15 رقماً يبدأ وينتهي بـ3 | صحّح الرقم |
| TIMESTAMP_FORMAT | خطأ | وقت بلا منطقة زمنية، أو تاريخ غير صحيح | استخدم 2026-04-18T13:30:00+03:00 أو new Date() |
| AMOUNT_FORMAT | خطأ | مبلغ بفاصلة عربية أو آلاف أو نوع غلط | 1150.00 بأرقام إنجليزية ونقطة |
| PHASE2_INCOMPLETE | خطأ | حقل من 6 و7 و8 ناقص، أو ختم بلا توقيع | مرّر الثلاثة مع بعض |
| TRUNCATED_TLV | خطأ | الرمز مقطوع عند فك الترميز | أعد قراءة الرمز كاملاً |
| INVALID_BASE64 | خطأ | حمولة فيها حروف خارج Base64 | تحقق من نص الرمز |
| INVALID_TLV | خطأ | علامة صفرية أو بنية غير صحيحة | الرمز ليس رمز زاتكا |
| AMOUNT_PRECISION | تنبيه | المبلغ مو بمنزلتين عشريتين | استخدم toZatcaAmount |
| VAT_EXCEEDS_TOTAL | تنبيه | الضريبة أكبر من الإجمالي | راجع حساب الفاتورة |
| PAYLOAD_TOO_LARGE | تنبيه | الحمولة طويلة وقد تفشل مع ماسحات الميدان | اختبر الرمز على جهاز حقيقي |
| DUPLICATE_TAG | تنبيه | نفس العلامة مكررة في الرمز | ارفض الرمز أو راجع مصدره |
| MISSING_REQUIRED_TAG | تنبيه | علامة إلزامية ناقصة عند القراءة | الرمز ناقص وليس فاتورة صحيحة |
| UNKNOWN_TAGS | تنبيه | علامات خارج المواصفة | تظهر في unknown وتُتجاهل |
| ZATCA_QR_ERROR | خطأ | الرمز الافتراضي حين لا يوجد رمز أدق | اقرأ نص الرسالة |
try {
const payload = encodeZatcaTlv(invoice, { strict: true })
} catch (error) {
if (error.code === 'FIELD_TOO_LONG') {
// الاسم عربي طويل، قصّره أو راجع بيانات البائع
}
}جرّبها
git clone https://github.com/exeerkit/zatca-qr
cd zatca-qr
python3 -m http.server 8080
# افتح http://localhost:8080/examples/demo.htmlأو افتح النسخة الجاهزة: https://exeerkit.github.io/zatca-qr/
صفحة عربية تولّد الرمز قدامك وتفككه بايت بايت، وكل شي يصير في متصفحك بدون ما يطلع حرف من جهازك.
node examples/node.mjs # مثال سريع بدون تثبيت
node --test # 38 اختبار بدون مكتباتالاختبارات
تشغل بدون أي تثبيت. فيها مثال جاهز لفاتورة بسيطة تحققنا منه بايت بايت (66 بايت):
01 0A "Acme Saudi" 02 0F "300000000000003" 03 14 "2026-04-18T10:30:00Z" 04 06 "115.00" 05 05 "15.00"
→ AQpBY21lIFNhdWRpAg8zMDAwMDAwMDAwMDAwMDMDFDIwMjYtMDQtMThUMTA6MzA6MDBaBAYxMTUuMDAFBTE1LjAwوفيه كمان اختبارات لطول الحروف العربية، والرموز المقطوعة، وBase64 الغلط، والتطابق بين الملفات.
أسئلة متكررة
تكفي للمرحلة الثانية؟ لا. هي تغلّف وتفك الرمز. التوقيع والشهادات شيء ثاني تتعامل معه بنفسك.
ليش ESM فقط؟
عشان تشتغل في المتصفح مباشرة بدون أدوات بناء. لو تستخدم CommonJS استخدم await import('zatca-qr').
ترسل بياناتي لخادم؟ لا. ما فيها أي اتصال بالنت أصلاً.
أقدر استخدمها في نظام تجاري؟
نعم، الرخصة MIT. والتأكد أن فاتورتك مطابقة هو مسؤوليتك، ولهذا موجود validateZatcaInvoice.
المراجع
الرخصة
MIT. استخدمها كيف ما تبي.
مين اللي سواها؟
هذه المكتبة جزء صغير من قالب VibeIO لبناء تطبيقات SaaS عربية.
القالب يجهز لك: دخول، فوترة، دفع، بريد، وملف AGENTS.md يوجه وكيلك الذكي.
حطيناها مفتوحة المصدر لأن مشكلة رمز زاتكا تواجه كل مطور سعودي. إذا عجبك الكود هنا، الأصل كله في vibeio.dev.
