راجِع API Reference · till/v1

توثيق ربط أنظمة الكاشير

باب واحد موحّد، تتصل فيه بنفس الطريقة أي شركة كاشير — تسجيل زيارة، استعلام عميل، استبدال نقاط، تطبيق عرض، وحجوزات. هذا الملف مولَّد من الكود نفسه: كل جدول حقول تحته يُقرأ من نماذج Pydantic اللي تتحقق منها المسارات فعلياً، لا موثَّق يدوياً — فما يفوت تحديثه أبداً.

المصادقة

بيانات اعتماد آلية واحدة لكل محل، صادرة كمنحة OAuth2 client-credentials. تُرسل كتوكن Bearer عادي:

Authorization: Bearer <your access token>

المحل (tenant) يُقرأ من التوكن نفسه، أبداً لا من جسم الطلب — ما فيه حقل tenant على أي طلب أدناه، عمداً. بيانات اعتماد صدرت لمحل واحد لا تقدر فيزيائياً تسمّي محلاً آخر — ما فيه حقل أصلاً يضبط ذلك.

صلاحية واحدة: operations.record. تقدر تسجّل زيارة، وتقرأ ما يخص عميل تلك الزيارة، وتسلّم مكافأة. لا شي غير ذلك — لا الفوترة، ولا محلات أخرى، ولا أي واجهة إدارية.

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

حدود الطلبات

استعلامات العميل (GET /customer) محسوبة كنسبة من الزيارات اللي فعلاً سجّلتها، لا نافذة ثابتة. كاشير مشغول يسجّل مبيعات فعلية ما يقترب من السقف أبداً؛ بيانات اعتماد تستعلم أكثر بكثير مما تسجّل تصطدم به، لأن هذا هو الشكل الوحيد اللي يأخذه استخراج البيانات. تجاوز السقف يرجّع 429 مع رسالة توضّح السبب — أبداً لا 403: بيانات الاعتماد لسا صالحة وما أُلغيت.

1. تسجيل الزيارات

POST /api/v1/till/visits

دفعة حتى 50 زيارة. الرد يحمل حكماً لكل عنصرaccepted، duplicate، أو rejected مع سبب — أبداً لا كل شي أو لا شي: عنصر واحد غير صالح دائمياً بدفعة لازم أبداً يوقف الـ49 عنصر الباقين خلفه بنفس الطلب.

طابق كل نتيجة برقمها (index) — موقع الزيارة بمصفوفة visits اللي أرسلتها (يبدأ من صفر). النتائج مرتّبة حسبه والقائمة بنفس طول ما أرسلت. لا تطابق على idempotency_key: لو ما أرسلت واحد، نشتقّه من رقم فاتورتك، والقيمة الراجعة ما كانت يوماً بحوزتك.

حقول الزيارة

الحقلالنوعمطلوبالسبب
phonestrلارقم جوال عميل المحل. بدونه ما نقدر ننسب الزيارة أو نراسله.
namestrلااسمه، إن كان متوفر. اختياري.
platestrلالوحة المركبة — تُستخدم عملياً بمحلات الإطارات وغسيل السيارات والتنجيد.
branch_idstrنعمالفرع اللي حصلت فيه. مطلوب، ولازم يكون فرعاً يملكه هذا المحل فعلاً — معرّف ما نعرفه يُرفض ويُسمّى لك بالرد.
operator_idstrلامعرّف حساب الكاشير عندك. يُخزَّن كما أُرسل — ما نبني نظام حسابات ثانٍ.
meterintلاقراءة العداد، لقطاع المركبات.
notestrلاملاحظة نصية حرة، تظهر لصاحب المحل.
lineslist[TillLineIn]لابنود الفاتورة. كتالوج المحل يُبنى منها تلقائياً — أرسلها كما هي.
invoice_idstrلارقم فاتورتك أنت. أفضل مفتاح تكرار، لأنه معرّفك الخاص لهذا البيع بالذات.
idempotency_keystrلامفتاح التكرار الخاص فيك، إن وُجد.
occurred_atintلاوقت حدوث البيع بساعتك أنت (ثوانٍ يونكس). يُستخدم كمفتاح تكرار إن ما فيه رقم فاتورة.
package_idstrلا

بند الفاتورة

الحقلالنوعمطلوبالسبب
product_idstrلامعرّف المنتج/الصنف عندك.
namestrلااسم المنتج/الخدمة كما بفاتورتك بالضبط — كتالوج المحل يُبنى منه.
qtyfloatلاالكمية.
amount_minorintلاالمبلغ بالهللات — أبداً لا ريالات، أبداً لا كسور عشرية.

إزالة التكرار — القاعدة الوحيدة بلا استثناء

كل زيارة لازم تحمل واحداً على الأقل من: idempotency_key، أو invoice_id، أو occurred_at. بدون أي من الثلاثة تُرفض الزيارة وتُسمّى الثلاثة بالسبب.

السبب: إعادة المحاولة بعد رد ضائع هي الحالة العادية للكاشير، وبلا مفتاح ثابت يصير بيع واحد زيارتين — يضاعف سجل العميل وتذكيراته ونقاطه.

مثال طلب

POST /api/v1/till/visits
Authorization: Bearer <token>
Content-Type: application/json

{
  "visits": [
    {
      "phone": "0501234567",
      "branch_id": "brn_main",
      "invoice_id": "INV-2026-00042",
      "lines": [
        {"product_id": "sku_wash_1", "name": "Full wash", "qty": 1, "amount_minor": 5000}
      ]
    }
  ]
}

مثال رد

{
  "accepted": 1,
  "total": 1,
  "results": [
    {"index": 0, "status": "accepted", "idempotency_key": "inv:<tenant>:INV-2026-00042"}
  ]
}

عنصر مرفوض يبدو هكذا:

{"index": 3, "status": "rejected",
  "reason": "unknown branch_id «brn-typo» — send one of this shop's own branches"}

2. البحث عن عميل

GET /api/v1/till/customer

استعلام بـphone أو plate. يجاوب هل هو عميل معروف، اسمه، عدد زياراته، رصيد نقاطه، وما يقدر يستبدله الآن.

عميل جديد يرجّع found: false، أبداً لا 404 — عميل أول مرة هو الحالة العادية عند الكاشير، مو خطأ يُسجَّل ويُعاد.

ثلاث حالات لا يجوز خلطها: ما فيه عميل بهذا الرقم · نقاط غير قابلة للقراءة مؤقتاً (points_unavailable: true) · ما فيه برنامج نقاط بهذا المحل أصلاً (قسم النقاط غائب تماماً). لا يُرسَل 0 مكان "ما قدرنا نقرأها" — إخبار عميل يملك 300 نقطة إنه بلا نقاط يخلق شكوى من لا شي.

مثال: عميل معروف، برنامج نقاط فعّال

{
  "found": true,
  "name": "Ahmad",
  "visits": 12,
  "points": 340,
  "to_next_reward": 60,
  "manager_threshold": 500,
  "rewards": [
    {"id": "rwd_free_wash", "name": "Free wash", "cost": 300, "needs_approval": false}
  ]
}

needs_approval: true على مكافأة تعني إنها فوق سقف اعتماد المدير بهذا المحل — شوف approved_by بمسار الاستبدال بالأسفل.

مثال: عميل جديد كلياً

{"found": false}

مثال: عميل معروف، نقاطه غير قابلة للقراءة مؤقتاً

{"found": true, "name": "Ahmad", "visits": 12, "points_unavailable": true}

3. استبدال مكافأة

POST /api/v1/till/redeem
الحقلالنوعمطلوبالسبب
phonestrلارقم جوال العميل المستبدِل.
platestrلاأو لوحته، إن كانت طريقتك بتعريف العملاء.
reward_idstrنعممعرّف المكافأة، بالضبط كما رجعت من GET /till/customer.
idempotency_keystrنعممفتاحك لهذي العملية بالذات — مطلوب. بلا مفتاح ثابت، إعادة المحاولة تصرف نقاط العميل مرتين.
actorstrلاحساب الكاشير اللي سلّم المكافأة.
approved_bystrلااسم/معرّف المدير المعتمِد — مطلوب فقط لمكافأة فوق سقف اعتماد هذا المحل (تُعلَم لك بـneeds_approval من GET /till/customer). بدونه، تلك المكافآت تُرفض دائماً. نص حر غير موثَّق — استخدم approver_operator_id/approver_ticket بالأسفل لهوية حقيقية موثَّقة.
approver_operator_idstrلامعرّف الموظف اللي تحقّقنا من PIN حقه للتو عبر POST /till/operators/verify-pin — المسار الموثَّق؛ يتجاوز approved_by عند إرساله.
approver_ticketstrلاالتذكرة الصادرة من POST /till/operators/verify-pin — صالحة لدقائق قليلة فقط.
branch_idstrنعمالفرع اللي حصل فيه — لازم فرعاً يملكه هذا المحل فعلاً.
idempotency_key مطلوب، بلا قيمة افتراضية نملأها عنك — مفتاح نصنعه نحن راح يكون جديداً بكل إعادة محاولة، واستبدال مكرر يصرف نقاط العميل مرتين.

الرفض يرجع كجملة كاملة — اقرأها للعميل مباشرة. فرّق بين REFUSED (نهائي — مثلاً رصيد غير كافٍ) وUNAVAILABLE (مؤقت — أعد المحاولة).

مثال طلب

{
  "phone": "0501234567",
  "reward_id": "rwd_free_wash",
  "idempotency_key": "redeem-2026-00042",
  "branch_id": "brn_main",
  "actor": "cashier_3"
}

أمثلة ردود

{
  "ok": true,
  "transaction_ref": "rdm_...",
  "discount_halalas": 5000,
  "reward_name": "Free wash",
  "cost": 300,
  "balance": 40,
  "duplicate": false
}

duplicate: true تعني إن هذا الـidempotency_key بالذات استُبدل من قبل — نفس الإيصال يُرجَّع، بلا صرف ثانٍ.

{"ok": false, "reason": "REFUSED", "message": "رصيده لا يكفي"}
{"ok": false, "reason": "NOT_FOUND", "message": "لا يوجد عميل بهذا الرقم."}

transaction_ref هو ما لازم تطبعه بالإيصال الورقي، عشان يكون له أثر ثانٍ.

4. تطبيق عرض على زيارة

POST /api/v1/till/offer/apply

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

الحقلالنوعمطلوبالسبب
visit_idstrنعممعرّف الزيارة المسجَّلة للتو — العرض يُختَم على هذي، أبداً لا على العرض نفسه.
phonestrلارقم جوال العميل حامل العرض.
platestrلاأو لوحته، إن كانت طريقتك.
actorstrلاحساب الكاشير اللي طبّق العرض.

مرة واحدة لكل زيارة — تطبيق ثانٍ على نفس الزيارة يرجع نفس العرض بلا صرف إضافي: ضغطة ثانية بالكاشير نية واحدة، لا خصمين.

مثال طلب

{"visit_id": "vis_...", "phone": "0501234567", "actor": "cashier_3"}

أمثلة ردود

{"applied": true, "offer": {"ref": "off_...", "text": "20% off your next wash",
  "used_at": 1735689600}}
{"applied": false, "reason": "لا يوجد عرضٌ ساري يشمل هذه الزيارة"}

5. الحجوزات

GET /api/v1/till/bookings
POST /api/v1/till/bookings/{booking_id}/attend

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

المسار الأول يسرد حجوزات العميل المفتوحة — أرسل جواله أو لوحته، يرجع لك أي شي لسا ما حضر أو انلغى أو تغيّب عنه:

{"found": true, "bookings": [{"id": "bkg_123", "starts_at": 1786860000, "branch_id": "brn_1", "service_ids": ["svc_1"], "package_id": ""}]}

المسار الثاني يؤكد الحضور فعلياً، ويحوّل الحجز إلى زيارة حقيقية:

الحقلالنوعمطلوبالسبب
phonestrلارقم جوال العميل صاحب الحجز.
platestrلاأو لوحته، إن كانت طريقتك.
branch_idstrلاالفرع اللي حصل فيه، إن أردت تأكيد إن الحجز يخص هذا الفرع بالذات. اختياري.
occurred_atintلاوقت الحضور الفعلي بساعتك (ثوانٍ يونكس). فارغ يعني الآن.

آمن لإعادة المحاولة — ضغطة ثانية على حجز حاضر بالفعل ترجع نفس الحجز بلا أثر إضافي: إعادة محاولة بعد انقطاع اتصال ما تقدر تضاعف الزيارة أو تصرف مكافأة مرتين.

{"ok": true, "booking_id": "bkg_123", "status": "attended"}

الرفض جملة تُقرأ بصوت عالٍ، أبداً لا كود مجرّد:

{"ok": false, "reason": "NOT_FOUND", "message": "لا يوجد حجز بهذا الرقم يخصّ هذا العميل."}

6. الفروع

GET /api/v1/till/branches

فروع محلك النشطة — تحقّق من branch_id قبل الإرسال، بدل ما تكتشف خطأ إملائي فقط برفض زيارة:

{"branches": [{"id": "brn-main", "name": "الفرع الرئيسي"}]}

7. حالة الاعتماد

GET /api/v1/till/status

هل ربطك البرمجي فعّال الآن فعلياً، وكم تبقى من سقف الاستعلام اليومي — اسأل هذا قبل إرسال دفعة مبيعات أو استعلام عميل، لا بعد ما تُرفض:

{"feature_api": "active", "feature_visit": "active", "lookup_allowance_remaining_today": 187}

"unknown" تعني ما قدرنا نسأل — لا رفض ولا اعتماد.

8. اعتماد موظف موثَّق

POST /api/v1/till/operators/verify-pin

قبل هذا الباب، أي نص بـapproved_by كان يُقبَل بلا أي تحقق. هذا يتحقق فعلياً من PIN الموظف الحقيقي المكوّن من 4 أرقام، ويصدر تذكرة قصيرة العمر — أرسلها مع approver_operator_id بمسار الاستبدال بدل اسم مكتوب حراً.

الحقلالنوعمطلوبالسبب
operator_idstrنعممعرّف الموظف — من قائمة فريق هذا المحل نفسها.
pinstrنعمرمز PIN الخاص بالموظف، 4 أرقام.
{"ok": true, "operator": {"id": "opr_1", "name": "خالد"}, "ticket": "...", "ticket_ttl": 300}

9. طلب تقييم فوري

POST /api/v1/till/rating-request

ما يبني مسار إرسال جديد — يسحب طلب تقييم مجدولاً أصلاً لهذا العميل ليصير "مستحقاً الآن"، فيصله خلال دقائق بدل تأخير المحل المعتاد. يجاوب برفض واضح، أبداً لا خطأ، لمّا ما فيه شي مجدول (بلا تطبيق تقييم، أو أُرسل مسبقاً):

الحقلالنوعمطلوبالسبب
phonestrلارقم جوال العميل صاحب الزيارة.
platestrلاأو لوحته، إن كانت طريقتك.
asset_idstrلامعرّف المركبة، لمحلات قطاع المركبات فقط — إن كان جدول التقييم مرتبطاً بالمركبة لا العميل.
{"ok": true, "message": "سيصل طلب التقييم للعميل خلال دقائق قليلة."}

جاهز تربط نظامك؟

نوفّر محلاً تجريبياً حقيقياً على نفس المسارات، مجاناً وبلا احتساب — راسلنا وناخذك خطوة بخطوة.

تواصل معنا للربط ↗