# Gradally — حزمة الربط مع منصة أحمد الجوهري

الإصدار: 20260918-1. هذه حزمة تقنية قابلة للمشاركة لبدء الربط. تصف ثلاث طرق منفذة ومختبرة. لكل منصة عنوانها الخاص على gradally.io (`https://<رمزكم>.gradally.io`) بعملية وقاعدة بيانات وبيانات دخول مستقلة؛ رمز المنصة و`stream_id` والأسرار تُسلَّم منفصلة عن الحزمة. لا تتضمن الحزمة مفاتيح أو بيانات طلاب أو تفويض نشر درجات. الصفحة `index.html` هي الدليل الكامل للمطور، وهي منشورة على https://gradally.io/docs/ والتواصل عبر contact@gradally.io.

## ثلاث طرق للربط — اختاروا الأقل عملًا عليكم

| | أ — الموصّل بلا عمل | ب — طلبان وwebhook | ج — الواجهة الكاملة |
|---|---|---|---|
| من ينقل الإجابات | Gradally تقرأها من قاعدة بياناتكم | خادمكم يرسلها | خادمكم يرسلها |
| من ينقل الدرجات | Gradally تكتب جدول نتائج واحدًا عندكم | Gradally ترسل webhook موقّعًا إلى عنوانكم | خادمكم يسحب سجل التغييرات ويقر |
| عملكم | منح وصول، إنشاء جدول واحد، السماح لعنواننا | طلب صادر لكل إجابة ومستقبِل HTTPS واحد | صندوق صادر ومستهلك ومؤشرات وجدول ظل وإقرارات |
| متى تناسب | يمكنكم فتح نسخة replica أو عرض view لنا | تعذر الوصول إلى قاعدة البيانات | تريدون التحكم الكامل والتدقيق عندكم |

### أ — الموصّل (موصى به)

- تمنحوننا مستخدم قاعدة بيانات للقراءة فقط على جدول الإجابات المكتوبة أو عرض (view) يحوي: معرّف صف متزايد، مرجع المحاولة، مرجع السؤال، نص الإجابة، وقت آخر تحديث، ومرجع الامتحان للتصفية. نقرأ هذه الأعمدة فقط؛ الأسماء والهواتف والبريد لا تُقرأ أبدًا.
- تنشئون جدول `gradally_results` من تعريف DDL الموجود في `index.html` مع مستخدم يكتب فيه فقط. لا نلمس جداول أعمالكم.
- تسمحون لعنوان Gradally العام بالوصول عبر TLS بشهادة موثقة، أو إلى نسخة replica.
- نتفق على خريطة مراجع الأسئلة مرة لكل امتحان.
- يعمل الموصّل كل 15 ثانية؛ كل تعديل على الإجابة يصبح إصدارًا جديدًا؛ النتيجة الصالحة هي أعلى إصدار بحالة `ready`، والإصدارات الأقدم تُعلَّم `superseded`. القيم `fraction` و`earned` و`maximum` نصوص عشرية؛ `NULL` ليس صفرًا.
- إن تعذر فتح قاعدة البيانات لكن لديكم نقطة HTTP تُرجع الإجابات المتغيرة منذ وقت معين، أخبرونا بشكلها؛ يستطيع الموصّل قراءتها بدل الجدول.

### ب — طلبان وwebhook موقّع

- خادمكم يرسل كل إجابة عند التسليم بـ`POST /answers/`، وGradally ترسل كل نتيجة متغيرة إلى عنوان HTTPS تحددونه، مع عنوان IPv4 العام له (نتصل بالعنوان المثبّت وبشهادة موثقة؛ العناوين الخاصة وIPv6 وإعادة التوجيه مرفوضة).
- الرؤوس: `X-Gradally-Webhook-Id` و`X-Gradally-Webhook-Timestamp` و`X-Gradally-Webhook-Signature` بصيغة `sha256=<hex>`؛ التوقيع HMAC-SHA256 على `timestamp + "." + body` بالبايتات كما وصلت. الجسم بصيغة `sanad-result-webhook-1` ومثاله في `examples.json`.
- تتحققون من التوقيع والزمن (±300 ثانية)، تخزنون الحدث بمفتاح (stream_id, event_id) ثم تردون 2xx. التسليم مرة على الأقل وبالترتيب لكل ربط، حتى 8 محاولات مع تراجع زمني. الرد 2xx تأكيد نقل فقط، لا إقرار ولا نشر.

### ج — الواجهة الكاملة

```mermaid
sequenceDiagram
    participant P as خادم المنصة
    participant S as API Gradally
    P->>P: حفظ إجابة الطالب وحدث إرسال دائم
    P->>S: POST answers — هوية الإجابة وإصدارها ونصها
    S-->>P: 202 + إيصال دائم
    Note over S: معالجة غير متزامنة
    P->>S: GET changes — المؤشر المحفوظ
    S-->>P: معرفات الإيصالات التي تغيرت
    P->>S: GET result — النتيجة الحالية
    S-->>P: الحالة والدرجة المطبقة إن وجدت
    P->>P: حفظ نسخة ظل مرتبطة بالإصدار
    P->>S: POST acknowledgements
    S-->>P: تأكيد الاستلام؛ publishable=false
```

سبعة مسارات تحت `/api/v1/streams/{stream_id}/`: خريطة الأسئلة، استقبال إجابة، النتيجة الحالية، استعادة الإيصال من معرف الإجابة، مسح الإيصالات، سجل التغييرات، والإقرار. تستقبل Gradally إجابة واحدة في كل طلب؛ لا يوجد مسار استقبال مصفوفة إجابات. يمكن للمنصة الاحتفاظ بطابور وإرسال الإجابات تدريجيًا. يبقى سجل التغييرات وسيلة الاستعادة في الخيارين أ وب.

## محتويات الحزمة

| الملف | الاستخدام |
|---|---|
| `index.html` | دليل المطور الكامل: الخيارات الثلاثة، تعريف جدول النتائج، الـwebhook، المسارات السبعة، الأمثلة المسجلة، حالات القبول، وملخص عربي |
| `openapi.json` | مواصفة OpenAPI 3.1.1 للمسارات السبعة وحدث الـwebhook والحقول والأخطاء |
| `postman.collection.json` | طلبات جاهزة للاستيراد في Postman، دون أسرار أو تنفيذ تلقائي |
| `postman.environment.json` | متغيرات فارغة تُملأ محليًا ببيانات بيئة الاختبار |
| `examples.json` | أمثلة مصطنعة للإجابة والإيصال والانتظار والنتيجة والإقرار والتغييرات وحدث الـwebhook |
| `INTEGRATION.en.md` | العقد التفصيلي بالإنجليزية: النسخ، المؤشرات، إعادة الإرسال، التخزين والإقرار، الـwebhook والموصّل |
| `ACCEPTANCE.ar.md` | المطلوب من الطرفين واختبارات قبول الربط |
| `MESSAGE_TO_PLATFORM.ar.txt` | رسالة مقترحة لإرسال هذه الحزمة إلى الفريق التقني |
| `partner_api_client.py` | عميل مرجعي بلغة Python 3 (المكتبة القياسية فقط): المسارات السبعة والتحقق من توقيع الـwebhook؛ بلا أسرار أو طلبات تلقائية |

العنوان `https://your-platform.gradally.io` في الملفات مثال يُستبدل برمز منصتكم، والمعرفات والبصمات في الأمثلة غير تشغيلية. لا توجد بيئة اختبار مشتركة: أول ربط لكم امتحان تجريبي على عنوانكم، وامتحانات الإنتاج روابط جديدة على العنوان نفسه بأسرار خاصة بها. في الخيار أ نحتاج وصول Gradally إلى قاعدة بياناتكم من عنوان IP نرسله لكم.

## التجربة الأولى

1. يسلّم فريق المنصة مفتاح اختبار مصطنع ومعرفات أسئلته ووصف معرف محاولة الطالب والإجابة. يُجهز الامتحان في Gradally أولًا، ثم ينشأ ربط خاص به. لا يوجد في هذا العقد API لإنشاء الامتحان أو إرسال مفتاح التصحيح.
2. الخيار أ: يسلّم فريق المنصة بيانات الاتصال بقاعدة البيانات (كلمة المرور عبر قناة منفصلة) وينشئ جدول النتائج ويسمح لعنواننا. يشغّل مسؤول Gradally الموصّل بوضع الخطة (قراءة فقط) ثم بالتنفيذ على إجابات مصطنعة، ويراجع الطرفان جدول النتائج.
3. الخيارات ب وج: يسلّم مسؤول Gradally رمز المنصة و`stream_id` وسر الربط الخاص بهذا الامتحان عبر قناة منفصلة (السر عشوائي من 32 إلى 200 حرف، لا تحتفظ Gradally إلا بتجزئته، ولا يُعرض مرة أخرى)، وفي الخيار ب يُسلَّم سر توقيع الـwebhook منفصلًا أيضًا. يستخدم السر للاتصال بخدمة Gradally من خادم المنصة فقط.
4. يستورد المطور ملفي Postman، ويملأ `base_url` و`stream_id` و`stream_token`. يجلب Manifest ويطابق `question_id` مع السؤال المتفق عليه، ثم ينسخ بصمته الحالية إلى `question_fingerprint`.
5. يرسل نصًا مصطنعًا، ويحفظ `receipt_id`. الإعادة المطابقة ترجع الإيصال نفسه. يجرب إجابة مع تعديل متتابع، وخطأ بصمة، وإعادة بعد انقطاع. في الخيار ب يتحقق من وصول الـwebhook وتخزينه مرة واحدة.
6. تُنفذ حالات القبول المشتركة على أسئلة وإجابات مصطنعة. يتفق الطرفان على معايير قبول النتائج قبل الانتقال إلى الاستخدام الفعلي.

لا يمثل إيصال الاستلام أو صف النتيجة وعدًا بانتهاء المعالجة خلال مدة محددة. يُتفق على زمن الخدمة بصورة منفصلة.

## حدود مهمة لفريق الربط

- المصادقة من الخادم فقط. لا يوضع الرمز في المتصفح أو تطبيق الطالب أو عنوان URL. فتح موقع آخر ببيانات الطالب في الرابط ليس مسارًا للدرجات.
- كل ربط يخص امتحانًا واحدًا داخل مؤسسة واحدة. لا يوجد تبديل مؤسسة بواسطة `X-Tenant-ID`.
- لا ترسل اسم الطالب أو بريده أو هاتفه أو درجته السابقة. تحتفظ المنصة بعلاقة المعرف المبهم بالطالب والمحاولة. الموصّل يختار الأعمدة المتفق عليها فقط.
- `202` يعني استلامًا، و`ready` يعني حكمًا مطبقًا داخل Gradally. `publishable=false` يعني أن الحزمة لا تسمح بإظهاره كدرجة نهائية للطالب. فترة الاختبار تستخدم تخزين الظل أو جدول النتائج.
- `null` أو التعليق أو الخطأ لا يعني صفرًا. حالات عدم الجاهزية لا تنشئ مهمة تصحيح يدوي للمدرس.
- ضاع الإيصال بعد انقطاع؟ أعد إرسال الطلب نفسه (يعود الإيصال نفسه)، أو اجلب `GET /submissions/?submission_id=…` لاستعادته مع نتيجته الحالية.
- حصة الاستقبال الحالية داخل المؤسسة: 600 طلب لكل ربط و1200 لكل IP خلال نافذة 60 ثانية، وتشمل الإعادات. يلتزم العميل بـ`Retry-After` وقد يفرض الخادم الوسيط حدًا إضافيًا.
- قراءة خريطة الأسئلة والنتائج والتغييرات وتأكيدات الاستلام لها حصة مشتركة منفصلة: 600 طلب لكل ربط و1200 لكل IP خلال 60 ثانية. لا تستهلك حصة استقبال الإجابات. عند `429` ينتظر العميل المدة المحددة ثم يعيد الطلب نفسه؛ الحصص ليست ضمانًا لسرعة المعالجة.

مرجع معنى الاستلام غير المتزامن: [HTTP 202 في RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.3). و[مواصفة OpenAPI](https://spec.openapis.org/oas/v3.1.1.html) هي تنسيق وصف العقد المرفق.
