ف فواترا

منع التكرار والمحاولات

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

الحل: ترويسة Idempotency-Key

أرسل مع كل POST /v1/invoices مفتاحًا فريدًا ثابتًا لتلك الفاتورة في نظامك (حتى 80 حرفًا):

Idempotency-Key: so-99813-attempt
# أو
Idempotency-Key: 8f14e45f-ea1c-4b7f-9c0d-2a3b4c5d6e7f

السلوك:

  • الطلب الأول ينشئ الفاتورة ويعيد 201 أو 202.
  • أي طلب لاحق بنفس المفتاح لا ينشئ شيئًا ويعيد 200 مع الفاتورة الأصلية والحقل idempotent_replay: true.
{
  "data": { "id": "inv_5c8d1e9a2b7f3046", "status": "accepted", "...": "..." },
  "idempotent_replay": true
}
المفتاح فريد لكل حساب ولا تنتهي صلاحيته — فهو مرتبط بسجل الفاتورة نفسه. استخدم معرّف المستند في نظامك (SO-99813) أو UUID تولّده وتخزّنه مع الفاتورة قبل أول محاولة إرسال.

الحماية الثانية: رقم الفاتورة

حتى لو نسيت المفتاح، لن تُسجَّل فاتورتان بنفس invoice_number في الوضع نفسه: المحاولة الثانية تُرفض برمز 409:

{
  "error": {
    "type": "duplicate_invoice_number",
    "message": "رقم الفاتورة مستخدم مسبقًا في هذا الوضع.",
    "existing_invoice_id": "inv_5c8d1e9a2b7f3046"
  }
}

استخدم existing_invoice_id لجلب الفاتورة القائمة بدل إنشاء واحدة جديدة.

نمط التعامل الموصى به

function sendInvoice(order) {
  // 1) خزّن مفتاح منع التكرار في جدولك قبل أي طلب
  if (!order.idempotency_key) {
    order.idempotency_key = crypto.randomUUID();
    db.save(order);
  }

  // 2) أرسل
  const res = await post('/v1/invoices', body, {
    'Idempotency-Key': order.idempotency_key
  });

  // 3) خزّن النتيجة أيًا كانت
  order.fawtara_id  = res.data.id;
  order.status      = res.data.status;      // accepted | rejected | error
  order.qr_content  = res.data.qr_content;
  db.save(order);

  // 4) ما لم تُقبل، لا تُعلن النجاح لمستخدمك
  if (order.status !== 'accepted') {
    notifyOps(res.data.error);
  }
}

إعادة المحاولة عند الأخطاء المؤقّتة

الحالةأعد المحاولة؟كيف
429 rate_limitedنعمانتظر قيمة Retry-After ثم أعد الطلب نفسه بنفس مفتاح منع التكرار.
500 / 503نعمتدرّج أُسّي: 1s، 2s، 4s، 8s — بحد أقصى 5 محاولات.
status = error (jofotara_unreachable)نعمPOST /v1/invoices/{id}/retry — لا تُنشئ فاتورة جديدة.
status = rejectedلاالبيانات خاطئة. صحّحها وأنشئ فاتورة برقم جديد.
422لاأصلح الحقول المذكورة في error.fields أولًا.
402لارقّ خطتك أو انتظر بداية الشهر.

ملاحظة عن عدّاد ICV

البوابة تطلب عدّادًا تصاعديًا لا يتكرّر لكل مكلّف. نحن ندير هذا العدّاد نيابة عنك داخل معاملة قاعدة بيانات، فلا تحتاج لإرساله أو تتبّعه. لاحظ أن كل محاولة إنشاء ناجحة تستهلك رقمًا جديدًا — بينما retry يُعيد إرسال نفس المستند بنفس الـ ICV، وهذا هو السلوك الصحيح.