تسجيل الدخول برمز OTP
الخلفية
على Shukah يسجّل التطبيق الدخول باستدعاءين. يرسل GET /maeda/api/reset-otp رمزًا عبر SMS،
ويستبدل GET /maeda/api/verify-otp الرمز بـ token. واستدعاء reset-otp نفسه يسجّل أيضًا عميلًا
جديدًا. تضم خدمة المصادقة (auth service) الـ routes الاثنين بأسماء الـ query نفسها عند Shukah، لذا فالتحوّل
في معظمه تغيير في base URL وبادئة المسار. وثلاثة أمور أخرى تستحق العناية: يُرجع verify-otp
JWT من Locate بدل token من Shukah، وأخطاؤه لها body مختلف، وهو يتجاهل
fcm_token وplatform اللذين يرسلهما التطبيق. كما أن reset-otp يصوغ خطأ الهاتف المفقود
بعبارة مختلفة.
الميزة
يستخدم تسجيل الدخول وإنشاء الحساب الاستدعاءين نفسيهما.
- يستدعي التطبيق reset-otp مع رقم الهاتف. إذا كان لعميل نشط (active) هذا الهاتف، ترسل الخدمة
رمزًا من 4 أرقام عبر SMS وتجيب
{"status":true}. - إذا لم يكن لأي عميل نشط هذا الهاتف ولم يحمل الاستدعاء حقول التسجيل، يجيب reset-otp
بـ
400معLast Name cannot be blank.. وهذه الرسالة هي إشارة التطبيق لفتح شاشة التسجيل. ثم يستدعي التطبيق reset-otp مجددًا مع حقول التسجيل، فتنشئ الخدمة العميل وترسل الرمز. - يستدعي التطبيق verify-otp مع الهاتف والرمز. فيحصل على JWT من Locate وعلى الملف الشخصي
للعميل، ويرسل الـ JWT في
Authorization: Bearer <token>مع كل استدعاء لاحق.
لا تنقل تسجيل الدخول إلى Locate بعد. فتسجيل الدخول في Locate يُرجع JWT فقط، وترفض routes النقاط في
order-v2 الـ JWT اليوم. وإلى أن يقبله order-v2، واصل تسجيل الدخول على Shukah، واحصل على الـ JWT عبر
exchange-token (target_platform: locate)، واحتفظ بالـ tokenين:
الـ JWT لخدمة المصادقة، وtoken Shukah لـ order-v2 (انظر
قائمة التحويل).
في نسخة التطبيق اللحظية بتاريخ 2026-02-26، يرسل تسجيل الدخول الهاتف بعد حذف علامة +، ويضيف التسجيل
الأسماء والبريد الإلكتروني، واختياريًا sex وbirth_date وreceive_offers. ويفتح التطبيق
شاشة التسجيل عندما يجيب reset-otp بـ 400 مع رسالة تتضمن Last Name، ويخزّن
access_token الذي يُرجعه verify-otp.
قواعد الرمز:
| القاعدة | القيمة |
|---|---|
| الطول | 4 أرقام. ويمكن أن يبدأ بـ 0. |
| مدة الصلاحية | 3 دقائق. ويمكن استخدامه مرة واحدة. |
| إعادة الإرسال | ما دام هناك رمز معلّق، فإن استدعاء reset-otp مجددًا لا يرسل شيئًا ويظل يجيب {"status":true}. ولا يمكن إصدار رمز جديد إلا بعد انتهاء صلاحية القديم أو استخدامه. |
| المحاولات | لا حد للرموز الخاطئة ولا حد لمعدل الطلبات (rate limit) على أي من الاستدعاءين. |
| وضع الاختبار | يحصل حساب وضع الاختبار دائمًا على الرمز 2022 ولا يُرسل SMS. أما أي أرقام على dev هي في وضع الاختبار: غير موثّق بعد. |
GET /api/v1/clients/reset-otp
GET /maeda/api/reset-otp✅ تم الاستبدال- 🟡 خطأ 400 لغياب الهاتف بالإنجليزية (Yii) على Locate وبالعربية (Laravel) على Shukah؛ ويعرض التطبيق message لذا يتغير النص.
- 🟡 يحصل الهاتف غير المعروف على 400 "Last Name cannot be blank." على Locate (بحسب الكود)؛ ونص Shukah الحالي غير مسجَّل.
- 🟡 لا يفتح التطبيق التسجيل إلا إذا احتوت رسالة 400 على "Last Name"؛ تأكد من اتفاق Shukah وLocate.
route واحد يخدم تسجيل الدخول والتسجيل معًا، كما في Shukah؛ وفي Locate يحصل الهاتف غير المعروف على 400 "Last Name cannot be blank." (وهي إشارة للتطبيق لفتح التسجيل؛ بحسب الكود، أما نص Shukah الحالي فغير موثّق) ولا يُقرأ receive_offers.
الطلب (Request)
لا حاجة إلى token. وعندما يعود الهاتف إلى عميل نشط تُتجاهل حقول التسجيل.
| الحقل | الموضع | النوع | مطلوب | القيود | الوصف |
|---|---|---|---|---|---|
contact_phone | query | string | نعم | تُحذف المسافات. ويُطابَق مع الهاتف المخزَّن بوجود + في البداية أو بدونه (+966… يساوي 966…، لكن 05… لا يساوي +9665…). لا فحص للطول أو الصيغة. الفارغ أو المفقود يعطي 400. | الهاتف المطلوب تسجيل الدخول به أو تسجيله. |
last_name | query | string | للتسجيل فقط | مطلوب عندما لا يكون لأي عميل نشط هذا الهاتف. الفارغ يعطي 400 مع Last Name cannot be blank. (يُفحص أولًا). بحد أقصى 100 حرف. | اسم العائلة للعميل الجديد. |
first_name | query | string | للتسجيل فقط | الفارغ يعطي 400 مع First Name cannot be blank. (يُفحص ثانيًا). بحد أقصى 100 حرف. | الاسم الأول للعميل الجديد. |
email_address | query | string | للتسجيل فقط | الفارغ يعطي 400 مع Email Address cannot be blank. (يُفحص ثالثًا). لا فحص لصيغة البريد. | البريد الإلكتروني للعميل الجديد. |
birth_date | query | string | لا | يُقرأ كتاريخ ويُخزَّن بالصيغة YYYY-MM-DD. والقيمة التي ليست تاريخًا تُخزَّن فارغة بصمت. | تاريخ ميلاد العميل الجديد. |
sex | query | 1 أو 2 | لا | 1 ذكر و2 أنثى. وأي قيمة أخرى تُخزَّن «غير معروف». | جنس العميل الجديد. |
language | query | string | لا | تُخزَّن الجزء الذي قبل - بأحرف صغيرة (ar-SA تصبح ar). والمفقودة تصبح ar. ولا تغيّر لغة SMS، التي هي دائمًا العربية. | لغة العميل الجديد. |
| أي مفتاح query أو header آخر | query، header | — | لا | يُتجاهل، مثل key. ولا يُقرأ receive_offers أيضًا: فالعميل الجديد يبدأ وهو معطَّل. | — |
الاستجابة (Response)
200 بالـ body نفسه لتسجيل الدخول وللتسجيل:
| الحقل | النوع | يقبل null | الصيغة / القيود | الوصف |
|---|---|---|---|---|
status | boolean | لا | دائمًا true. | أُصدر رمز، أو يوجد رمز معلّق بالفعل. ولا يؤكد أن SMS قد أُرسلت. |
{
"status": true
}
التُقط على dev، 2026-10-02، لهاتف معروف، على Locate وعلى Shukah: وأجابتا بهذا الـ body.
وأرسل الاستدعاء رسالة SMS واحدة. والـ body نفسه موجود في docs/api/client-auth-and-address-book.md §1
(locate-authentication-service@6757aa4، آخر تغيير للوثيقة 2026-07-27).
الأخطاء
| HTTP | Body | متى |
|---|---|---|
400 | {"name":"Bad Request","message":"Phone number cannot be blank.","code":0,"status":400,"type":"yii\\web\\BadRequestHttpException"} | contact_phone مفقود أو فارغ. |
400 | الشكل نفسه، مع ضبط message على Last Name cannot be blank. أو First Name cannot be blank. أو Email Address cannot be blank.. | لا عميل نشط بهذا الهاتف، وأول حقل تسجيل فارغ (بهذا الترتيب) هو المسمّى. والاستدعاء المجرد لتسجيل الدخول بهاتف غير معروف يحصل على Last Name cannot be blank.. |
500 | {"status":500,"message":"Internal server error"} | فشل غير متوقع، مثل اسم أطول من 100 حرف عند التسجيل، أو خطأ في قاعدة البيانات، أو بوابة SMS لا تعطي استجابة HTTP. لم يُلتقط. وخارج production تكون الرسالة نص الخطأ الخام ويُضاف حقل stack. |
التُقط body الخاص بـ Phone number cannot be blank. على dev في 2026-10-01 (استدعاء دون أي
parameters). أما body الخاص بـ Last Name cannot be blank. فمن docs/api/client-auth-and-address-book.md
§1 (2026-07-27): فجولة الالتقاط لم تجرِ أي استدعاء برقم هاتف، لأن الاستدعاء قد يرسل SMS.
جرّبه
الاستدعاء الناجح يرسل SMS حقيقية ما لم يكن الرقم لحساب في وضع الاختبار. استخدم رقمًا في وضع الاختبار تتحكم به. أي الأرقام في وضع الاختبار على dev: غير موثّق بعد.
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export PHONE='<test-mode-phone>' # placeholder: replace it with a test-mode number
curl -s -G "$AUTH_BASE_URL/api/v1/clients/reset-otp" --data-urlencode "contact_phone=$PHONE"
المتوقع: 200 و{"status":true}. وبلا هاتف تحصل على 400 المبيّن تحت «الأخطاء».
الفروق عن Shukah
| الجانب | Shukah | Locate | الأثر على التطبيق |
|---|---|---|---|
| URL | GET $SHUKAH_BASE_URL/maeda/api/reset-otp | GET $AUTH_BASE_URL/api/v1/clients/reset-otp. تتجاهل Locate key وكل مفتاح query مجهول آخر وكل header. | غيّر base URL والمسار. ويمكن للتطبيق مواصلة إرسال key وheaders الخاصة به. |
| خطأ الهاتف المفقود | {"name":"Bad Request","message":"حقل contact phone مطلوب.","code":0,"status":400,"type":"Illuminate\\Validation\\ValidationException"} (التُقط على dev، 2026-10-01): رسالة بالعربية وtype من Laravel. | {"name":"Bad Request","message":"Phone number cannot be blank.","code":0,"status":400,"type":"yii\\web\\BadRequestHttpException"} (التُقط على dev، 2026-10-01): رسالة بالإنجليزية وtype من Yii. | المفاتيح نفسها، لكن النص وtype يختلفان. لا تعتمد على هذا النص في المطابقة. |
| هاتف غير معروف، بلا حقول تسجيل | 400 مع Last Name cannot be blank. في body بنمط Yii، في التقاط يوليو 2026 المحفوظ في الوثيقة المسودة لخدمة المصادقة (انظر Sources تحت «التفاصيل»). نص Shukah اليوم: غير موثّق بعد. | 400 مع Last Name cannot be blank. في body بنمط Yii. | تستمر إشارة التسجيل في العمل إن كانت Shukah ما زالت ترسل النص نفسه اليوم. أكّد نص Shukah الحالي قبل الاعتماد عليه. |
| body النجاح | {"status":true} (التُقط على dev، 2026-10-02). | {"status":true} (التُقط على dev، 2026-10-02). | لا شيء. |
GET /api/v1/clients/verify-otp
GET /maeda/api/verify-otp✅ تم الاستبدال- 🔴 لا تنقل تسجيل الدخول بعد: يُرجع تسجيل الدخول في Locate JWT فقط، وترفضه routes النقاط في order-v2 اليوم.
- 🟡 body الـ 400 عند الرمز الخاطئ هو {status,message}؛ وتضيف Shukah الحقول name/code/type (Laravel)؛ ويقرأ التطبيق message فقط.
- 🟡 يُتجاهل fcm_token وplatform على Locate؛ ولا يظهر في استجابة Shukah ما تفعله بهما.
- 🟡 استدعاء ناجح على dev (2026-10-02): نفس {access_token,data} وملف شخصي بـ 13 مفتاحًا على الجانبين؛ وتبقى الجلسات السابقة صالحة.
يُرجع access_token وملفًا شخصيًا في data، حيث الـ token هو JWT من Locate (وbody النجاح في Shukah له الشكل نفسه، التُقط في 2026-10-02)، ويُتجاهل fcm_token وplatform.
الطلب (Request)
لا حاجة إلى token.
| الحقل | الموضع | النوع | مطلوب | القيود | الوصف |
|---|---|---|---|---|---|
contact_phone | query | string | نعم | تُحذف المسافات. ويُطابَق مع الهاتف الذي أُرسل إليه الرمز، بوجود + في البداية أو بدونه. المفقود يعطي 400 نفسه الذي يعطيه الرمز الخاطئ. | الهاتف المستخدم في reset-otp. |
code | query | string | نعم | يُقارَن كنص مع الرمز المكوَّن من 4 أرقام، لذا فالصفر في البداية مهم. | الرمز الوارد في SMS. |
language وplatform وfcm_token وأي مفتاح query آخر | query | string | لا | يُتجاهل. | — |
الاستجابة (Response)
200 مع الـ token والملف الشخصي:
| الحقل | النوع | يقبل null | الصيغة / القيود | الوصف |
|---|---|---|---|---|
access_token | string | لا | JWT من Locate: ثلاثة أجزاء تفصلها نقاط، موقَّع بـ HS256. لا تنتهي صلاحيته أبدًا. | أرسله في Authorization: Bearer <access_token> مع كل استدعاء لاحق. |
data | object | لا | الملف الشخصي للعميل: الحقول الـ 13 المدرجة تحت التحقق من الـ token. | العميل المسجَّل الدخول. |
{
"access_token": "<locate-jwt>",
"data": {
"client_id": 356259,
"odoo_id": "1342170",
"contact_phone": "+966500000000",
"status": "active",
"email_address": "user@example.com",
"first_name": "Test",
"last_name": "User",
"sex": 1,
"birth_date": "1990-01-01",
"receive_offers": 0,
"push_enabled": 0,
"is_food_truck_user": 1,
"is_cod_disabled": 0
}
}
الشكل من docs/api/client-auth-and-address-book.md §2 (locate-authentication-service@6757aa4، آخر تغيير للوثيقة 2026-07-27)، واستُدعي مباشرة على dev في 2026-10-02 (200، بالمفاتيح والقيم نفسها كما أعلاه؛ انظر «الفروق عن Shukah»). أُخفيت قيمة الـ token عند الالتقاط، فلم يُسجَّل هنا نوعه: وادعاء JWT مأخوذ من الكود. وقيم الملف الشخصي هي القيم التوضيحية المستخدمة في الموقع.
الأخطاء
| HTTP | Body | متى |
|---|---|---|
400 | {"status":400,"message":"Sorry, but your verification code is incorrect"} | لا يوجد رمز غير مستخدم وغير منتهي يطابق الهاتف والرمز (خاطئ أو منتهي أو مستخدم من قبل أو نقص في parameter)، أو لا عميل نشط بهذا الهاتف. رسالة واحدة تشمل كل ذلك، عن قصد، حتى لا يكشف الاستدعاء أي الهواتف لها حساب. |
400 | الشكل نفسه، مع نص الخطأ الخام في message. | أي فشل آخر داخل الـ handler، مثل خطأ في قاعدة البيانات أو فشل كتابة الـ token. لم يُلتقط. |
التُقط أول body على dev في 2026-10-01 (استدعاء دون أي parameters)، ومرة أخرى في 2026-10-02 برمز خاطئ. وهو ليس بنمط Yii، بخلاف أخطاء reset-otp.
جرّبه
الاستدعاء الناجح يستهلك الرمز وينشئ token جديدًا من Locate. وتبقى الـ tokens السابقة صالحة.
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export PHONE='<test-mode-phone>' # the number you used in reset-otp
export CODE='<4-digit-code>' # 2022 for a test-mode account
curl -s -G "$AUTH_BASE_URL/api/v1/clients/verify-otp" --data-urlencode "contact_phone=$PHONE" --data-urlencode "code=$CODE"
المتوقع: 200 مع access_token وdata. والرمز الخاطئ أو المنتهي يعطي 400 المبيّن تحت «الأخطاء».
الفروق عن Shukah
| الجانب | Shukah | Locate | الأثر على التطبيق |
|---|---|---|---|
| URL | GET $SHUKAH_BASE_URL/maeda/api/verify-otp | GET $AUTH_BASE_URL/api/v1/clients/verify-otp | غيّر base URL والمسار. |
| الـ Token | token معتم (opaque) من Shukah. | JWT من Locate في access_token. | كل استدعاء لاحق إلى /api/v1/clients/* يحتاج هذا الـ JWT، لكن routes النقاط في order-v2 ترفضه اليوم، لذا واصل تسجيل الدخول على Shukah حالياً. |
| مفاتيح query إضافية | يرسل التطبيق أيضًا fcm_token (token الـ push الخاص به، ويُحذف عندما يتعذّر على التطبيق الحصول عليه) وplatform (Android أو iOS أو Huawei). ما تفعله Shukah بهما: غير موثّق بعد. | تقرأ Locate contact_phone وcode فقط، فتُسقط fcm_token وplatform. | يظل الاستدعاء يعمل. وإن كانت Shukah تستخدم أيًّا من القيمتين فإن ذلك يضيع على Locate: أكّد ما كانت تفعله Shukah بهما. |
| خطأ الرمز الخاطئ | {"name":"Bad Request","message":"Sorry, but your verification code is incorrect","code":0,"status":400,"type":"Symfony\\Component\\HttpKernel\\Exception\\HttpException"} (التُقط على dev، 2026-10-02). وكان في التقاط يوليو 2026 المحفوظ في الوثيقة المسودة لخدمة المصادقة الرسالة نفسها مع type بنمط Yii هو yii\\web\\BadRequestHttpException (انظر Sources تحت «التفاصيل»). | {"status":400,"message":"Sorry, but your verification code is incorrect"} (التُقط على dev، 2026-10-01 و2026-10-02). | message وstatus موجودان في الاثنين، بالنص نفسه. أما name وcode وtype فقد اختفت، فلا تقرأها. |
| body النجاح | 200 {access_token, data}، حيث data هو الملف الشخصي ذو الحقول الـ 13 (التُقط على dev، 2026-10-02، بالهاتف والرمز نفسيهما كما في Locate). | 200 {access_token, data}، حيث data هو الحقول الـ 13 نفسها بقيم متساوية (التُقط على dev، 2026-10-02). | الشكل والملف الشخصي متطابقان، فلا يحتاج تحليل التطبيق إلى تغيير. أُخفيت قيمة access_token عند الالتقاط، فلم يُسجَّل هنا نوع الـ token على كل جانب (صف Token مأخوذ من الكود). ولم ينهِ تسجيل الدخول الجديد الجلسات السابقة على أي من الجانبين. |
تنبيه
- استدعاء reset-otp ثانٍ خلال 3 دقائق لا يرسل SMS جديدة لكنه يظل يجيب
{"status":true}، فلا تعنيtrueأن SMS أُرسلت. - العميل غير النشط (موقوف أو محظور أو معلّق أو منتهٍ) يُعامل كهاتف غير معروف، فيُرسَل إلى التسجيل.
- أجسام الأخطاء (error bodies) تختلف: أخطاء reset-otp بنمط Yii (
nameوmessageوcodeوstatusوtype)، أما أخطاء verify-otp فهيstatusوmessageفقط.
التفاصيل (للـ backend)
فُحص dev فقط: الـ host https://dev.authentication.locate.sa والبادئة /api/v1. أما الـ host والبادئة في UAT
وproduction: غير موثّق بعد. لا يعمل dev بـ NODE_ENV=production: فأجسام أخطائه
تحمل حقل stack (التُقط في 2026-10-01 على أخطاء address-book وprofile وsetting)، والـ handler لا يضيف stack إلا خارج production (src/middlewares/error-handler.js:12-35).
قواعد OTP
| القاعدة | الحقيقة | المصدر |
|---|---|---|
| الطول | 4 محارف، أرقام عشرية، وقد تبدأ بـ 0: Math.random().toString().slice(2, 6). | src/services/client-otp.js:7-8 |
| الصلاحية | 3 دقائق من لحظة الإنشاء. ويشترط verify أن يكون expired_at الآن أو بعده. | src/services/client-otp.js:5,37؛ src/controllers/client/verify-otp/handler.js:27 |
| الاستخدام مرة واحدة | يضبط verify القيمة verified: true وverified_at، ولا يطابق الرمز المستخدم بعد ذلك. | src/controllers/client/verify-otp/handler.js:26,38-41 |
| إعادة الإرسال | إذا وُجد رمز غير مُتحقَّق منه وغير منتهٍ للهاتف المخزَّن، فلا يُنشأ شيء ولا يُرسل شيء وتظل الإجابة {"status":true}. | src/services/client-otp.js:20-23 |
| المحاولات وحدود المعدل | لا شيء: لا عدّاد ولا قفل ولا محدِّد معدل (rate limiter). | docs/plans/2026-07-28-k6-load-test-suite.md:9 |
الرمز الثابت 2022، حساب وضع الاختبار | account_mode بقيمة TEST يعطي 2022 ولا SMS. | src/services/client-otp.js:7-8,40-42؛ src/shared/constant/user.js:27-30 |
الرمز الثابت 2022، هاتفان | الهاتف المخزَّن الذي يساوي تمامًا +966500000000 أو 966500000000 يحصل على 2022 في أي وضع، وتُرسل SMS ما لم يكن الحساب في وضع الاختبار. أُضيف في 2026-07-14 (commit 7aad3af). والهاتف النموذجي المستخدم في هذه الصفحة أحد هذين. | src/services/client-otp.js:25-27 |
| كيف يُضبط وضع الاختبار | القيمة الافتراضية لـ account_mode هي STANDARD. ولا يضبطه أي API: بل يُكتب مباشرة في قاعدة البيانات. | src/models/client.js:87-90؛ docs/plans/2026-07-28-k6-load-test-suite.md:18 |
| أرقام الاختبار | لا شيء موثّق لـ dev. تحجز مجموعة اختبار الحمل (load test) هواتف وضع الاختبار المكوَّنة من 9665000 يليها ستة أرقام (معرّفات العملاء من 9,000,000,000)، وتُزرع مباشرة في قاعدة البيانات. وهل توجد حسابات كهذه على قاعدة بيانات dev المشتركة: غير موثّق بعد. | docs/plans/2026-07-28-k6-load-test-suite.md:19,33-40؛ k6/seed/seed-test-clients.js:23-24,40-58؛ k6/README.md:13-23 |
| البحث عن الحساب | يجد reset-otp وverify-otp والبحث عن العميل (client lookup) العميلَ بالهاتف المخزَّن مع + اختيارية، غير محذوف، بحالة ACTIVE، بأحدث client_id أولًا، دون ذاكرة مؤقتة (uncached). والعميل غير النشط غير مرئي لهذه الثلاثة. | src/controllers/client/reset-otp/handler.js:24-30؛ src/shared/helpers/filter.js:1-4؛ docs/api/client-auth-and-address-book.md:52-54 |
| SMS | POST ${SMS_API}/v1/messages مع Bearer ${SMS_TOKEN}. النص بالعربية مهما كانت قيمة language. والمستلم 0XXXXXXXXX يصبح 966XXXXXXXXX، وإلا أُرسل الهاتف المخزَّن كرقم. وتُحفظ استجابة البوابة في صف OTP. | src/services/client-otp.js:11-14,30,41؛ src/shared/helpers/sms.js:4-24 |
| التسجيل في log | يُكتب الرمز المولَّد في log الخادم. | src/services/client-otp.js:29 |
التسجيل عبر reset-otp
عندما لا يكون لأي عميل نشط هذا الهاتف وتكون الحقول الثلاثة المطلوبة موجودة، ينشئ الـ handler
عميلًا ثم يصدر الرمز (src/controllers/client/reset-otp/handler.js:48-73).
- يحصل العميل الجديد على
client_id = 1,000,000,000 + counter، وsource_systemوchange_originمضبوطين علىlocate، وfrom_locate: true، ومعرّفات Shukah فارغة، وclient_language، وقيم النموذج الافتراضية: الحالةACTIVE، والوضعSTANDARD، والـ push مفعّل، والعروض معطّلة (src/controllers/client/reset-otp/handler.js:51-69،src/models/counter.js:7,17-24،src/models/client.js:83-90,110-111). وتُخزَّن الأسماء بعد حذف المسافات الزائدة ويسمح النموذج بـ 100 حرف لكل منها (src/models/client.js:45-52). - بعد إصدار الرمز، تدفع الخدمة العميل إلى Shukah:
POST {SHUKAH_HOST}/maeda/client، وهي عملية upsert بحسب الهاتف. وهي بذل أقصى جهد (best effort) ويُنتظر إتمامها، وتخزّن المعرّف الذي تُرجعه Shukah بوصفهshukah_id. ولا تفعل شيئًا عندما تكون المزامنة الصادرة (outbound sync) معطّلة أو لا يكون الـ host أو token المسؤول مضبوطًا (src/controllers/client/reset-otp/handler.js:71-73،src/services/shukah-sync.js:21-30,184-234). - الهاتف غير النشط يتصرف كهاتف غير معروف. ولا يعلن النموذج أي فهرس فريد (unique index) على
phone، فمع كل حقول التسجيل سيُدخل الـ handler عميلًا ثانيًا بالهاتف نفسه، وسيفوز ذلك العميل في البحث (أعلىclient_id). قراءة شيفرة، لم تُختبر مباشرة. وهل هذا مقصود: غير موثّق بعد (src/models/client.js:147-156). - ينتظر الطلب بوابة SMS (
await sendMessage، بلا مهلة). واستجابة البوابة بخطأ HTTP تُحفظ في صف OTP وتُتجاهل، فيظل الرد{"status":true}. أما فشل البوابة دون أي استجابة HTTP فيُلقي استثناءً داخل catch ويعطي500، بعد أن يكون صف OTP قد أُنشئ بالفعل، فلا تُرسل إعادة المحاولة خلال 3 دقائق شيئًا (src/shared/helpers/sms.js:18-23؛ قراءة شيفرة).
التحقق من الرمز
ترتيب العمليات (src/controllers/client/verify-otp/handler.js:20-46): إيجاد الرمز، وإيجاد
العميل النشط، وإنشاء JWT، وإدراج صف token_bridge، ودفع الـ JWT إلى Shukah، وتعليم الرمز بأنه مستخدم،
ثم الرد. ولا يُستهلك الرمز إلا بعد انتهاء كتابة الـ token والدفع.
- شكل النجاح
{access_token, data}مثبَّت باختبار وحدة (unit test) (src/controllers/client/__tests__/verify-otp.test.js:77-94). - كل نجاح ينشئ token جديدًا. وتبقى الـ tokens السابقة صالحة لأنه لا شيء يُبطلها
(
src/shared/helpers/jwt.js:4-10). أُكّد مباشرة على dev في 2026-10-02: بعد تسجيل دخول جديد على كل جانب، ظل الـ tokens اللذان صدرا سابقًا يعملان. - لصف
token_bridgeالقيمةsource_system: 'merchant'، ولا يحمل الـ token الخام إلا عندما لا يكونNODE_ENVproduction(src/services/merchant-token-bridge.js:6-16). - الدفع إلى Shukah هو
POST {SHUKAH_HOST}/maeda/client/add-tokenمع{access_token, client_id: <shukah_id>}، بذل أقصى جهد ويُنتظر إتمامه. ولا يفعل شيئًا عندما لا يكون للعميلshukah_idأو تكون المزامنة معطّلة (src/services/shukah-sync.js:13-75). وبهذه الطريقة تتعرف Shukah على JWTs من Locate (docs/plans/2026-07-16-shukah-token-outbound-push.md:10-28). وهل الدفع مفعّل على dev، وأي واجهات من Shukah تقبل JWT من Locate بعده: غير موثّق بعد. - أي استثناء في الـ handler (خطأ في قاعدة البيانات، أو فشل كتابة الـ token، أو تكرار hash للـ token) يُلتقط
ويُجاب عنه بـ
400مع الرسالة الخام (src/controllers/client/verify-otp/handler.js:47-53؛ قراءة شيفرة). - استدعاءا verify للعميل نفسه في الثانية نفسها سيُنشئان JWT متطابقًا بايتًا ببايت ويصطدمان
بفهرس
token_hashالفريد، فينتج400المذكور. قراءة شيفرة وفحص حتمية (determinism) دون اتصال، لم يُعاد إنتاجه: هل يحدث على dev: غير موثّق بعد. - لا يوجد عدّاد محاولات: فيمكن تجربة الرمز المكوَّن من 4 أرقام مرارًا خلال دقائقه الثلاث.
حيث تختلف الوثائق عن الشيفرة (القول للشيفرة)
- يقول مخطط التدفق الختامي في وثيقة الخدمة إن verify-otp يُرجع
{id, token}(docs/api/client-auth-and-address-book.md:542). أما الشيفرة، ووثيقة الخدمة نفسها في الأسطر 110-131، فتقولان{access_token, data}. - تقول وثيقة الخدمة إن verify «يحذف كل رموز OTP للهاتف»
(
docs/api/client-auth-and-address-book.md:135). أما الشيفرة فتعلّم الرمز المطابق وحده بأنه مستخدم (commita5a5315، 2026-07-17). - تقول وثيقة تصميم إن الرمز الخاطئ يعطي
400بنمط Yii (docs/plans/2026-07-06-shukah-auth-api-local.md:101-104). أما الشيفرة ووثيقة الـ API فتعطيان{"status":400,"message":...}. - رمز وضع الاختبار هو
2022في الشيفرة. أما اختبار الوحدة (src/services/__tests__/client-otp.test.js:6) ووثيقة تصميم (docs/plans/2026-07-06-shukah-auth-api-local.md:155,213) فتقولان8888، وهذا الاختبار يفشل أمام الشيفرة. - تقول وثيقة تصميم إن verify-otp يضبط
last_login_atويخزّن الـ token على العميل (docs/plans/2026-07-06-shukah-auth-api-local.md:106-108). والـ handler لا يفعل أيًّا منهما. - تكتب وثيقة تصميم 2026-07-06 والمخطط التدفقي المسودة المسارات بالشكل
/maeda/api/*. ومنذ commitd837492(2026-07-10) صار كل route للعميل تحتPREFIX_API_V1/clients، ويجيب host المصادقة بـ404على/maeda/*. - 2026-10-02: قالت هذه الصفحة إن reset-otp الناجح لم يُلتقط. وفي 2026-10-02 أجابت كل من Shukah
وLocate بـ
200 {"status":true}لهاتف معروف. - 2026-10-02: لم تعطِ هذه الصفحة body حاليًا لـ Shukah لرمز verify-otp خاطئ. وتجيب Shukah على dev
بـ body من Laravel مع
typeمن Symfony، يظهر تحت «الفروق عن Shukah». - 2026-10-02: قالت هذه الصفحة إن verify-otp الناجح لم يُلتقط مباشرة، وإن body النجاح في Shukah غير موثّق. وفي 2026-10-02 أجابت كل من Shukah وLocate بـ
200 {access_token, data}مع الملف الشخصي نفسه بـ 13 مفتاحًا وقيم متساوية، وظلت الـ tokens السابقة تعمل بعد تسجيل الدخول الجديد.
Sources
locate-authentication-service@6757aa4:src/routes/client.js:11-12،src/controllers/client/reset-otp/handler.js:11-30,34-74،src/controllers/client/verify-otp/handler.js:14-53،src/controllers/client/__tests__/verify-otp.test.js:77-94,src/services/client-otp.js:5-45،src/shared/helpers/sms.js:4-24,src/shared/helpers/filter.js:1-4،src/sync/lib/casts.js:2-18,35-41,src/shared/errors/yii-bad-request-error.js:12-25،src/middlewares/error-handler.js:10-35,src/shared/constant/user.js:14,27-30،src/shared/helpers/jwt.js:4-10,src/services/merchant-token-bridge.js:6-16،src/services/shukah-sync.js:13-75,184-234,src/models/client.js:45-52,83-90,110-111,147-156،src/models/counter.js:7,17-24,src/services/__tests__/client-otp.test.js:6،k6/seed/seed-test-clients.js:23-24,40-58,k6/README.md:13-23- وثائق
locate-authentication-service@6757aa4:docs/api/client-auth-and-address-book.md§1 و§2 (الأسطر 45-146، 542؛ آخر تغيير 2026-07-27)،docs/draft/api-auth-draft.md:5-31,90,109-116(2026-07-07)،docs/draft/flow-chart-shukah2locate.md:60,140-142(2026-07-08)،docs/plans/2026-07-06-shukah-auth-api-local.md:8-11,26,101-108,155,213,237-239،docs/plans/2026-07-16-shukah-token-outbound-push.md:10-28،docs/plans/2026-07-28-k6-load-test-suite.md:9,18-19,33-40 - نسخة الجوال اللحظية
mobile/locate-client@e960c3a(الفرعdevelopment، 2026-02-26؛ شيفرة التطبيق نفسه، وليست من Locate):src/entities/user/model/api/services/auth.service.ts:22-31,45-54,65-74،src/entities/user/model/api/dto/auth.dto.ts:43-88,103-122،src/shared/lib/helpers/phoneNumber/getClearPhoneNumber.ts:1-3،src/shared/lib/helpers/phone/getPlatform.ts:4-14،src/features/SignInForm/model/services/enterPhoneAuth.ts:48,53-67،src/shared/api/reqError.ts:125-127،src/features/RegisterNewUserForm/model/services/registerNewUser.ts:105،src/entities/user/model/services/sendCode/sendCode.ts:28-35،src/features/VerificationCodeForm/model/services/sendCode.ts:46-57 - التقاطات dev، 2026-10-02 (parity audit):
reset-otp--shukahوreset-otp--locateوreset-otp-missing--shukahوreset-otp-missing--locateوverify-otp-wrong--shukahوverify-otp-wrong--locate - التقاطات dev، 2026-10-01:
probe-locate-reset-otpوprobe-locate-verify-otpوprobe-shukah-reset-otpوprobe-auth-host-maeda-prefix