المواصفة 02 — عقود واجهة API (NestJS، REST /api/v1)
الاصطلاحات: الصيغة JSON؛ التحقق من صحة كائنات النقل DTO عبر class-validator تحققاً صارماً — فالخصائص غير المعروفة تُرفض ولا تُحذف؛ وأحجام متون الطلبات محدودة؛ شكل الأخطاء { statusCode, code, message, messageAr }؛ التقسيم إلى صفحات ?page=1&limit=20 ← { items, total, page, limit }؛ المصادقة عبر كوكيز من نوع httpOnly هي nutqi_at / nutqi_rt؛ مُزخرِفات الحراسة @Roles(...)؛ وكل ذلك موصوف بتعليقات Swagger ولا يُقدَّم في الإنتاج إلا إذا فُعِّل صراحةً.
ومسارات القراءة تردّ صفوفاً مسطَّحة (D-26): الأسماء محلولةً في نص واحد، والتواريخ التقويمية بصيغة YYYY-MM-DD، والصور الرمزية روابط جاهزة للاستعمال، والعدّادات محسوبةً في الخادم، والعلاقات محذوفة — فلا يتسرّب تخطيط قاعدة البيانات إلى أي عميل.
وتحديد المعدّل يجري لكل عنوان شبكة، بميزانية أضيق بكثير على /auth/login ومسارات رمز التأكيد؛ وهو مكمِّل لقفل الحساب في G15 لا بديل عنه. والمتغيّران JWT_ACCESS_SECRET وCORS_ORIGIN مطلوبان في الإنتاج — فالتطبيق يرفض الإقلاع بدل الارتداد إلى قيمة افتراضية غير آمنة.
التوثيق التفاعلي (Swagger / OpenAPI)#
كل ما يلي هذا القسم مكتوب باليد، وقائمة مسارات مكتوبة باليد تنحرف عن الحقيقة فور أن يضيف أحدهم مساراً جديداً. أما التوثيق التفاعلي فلا ينحرف: تبنيه حزمة @nestjs/swagger عند الإقلاع من المُزخرِفات نفسها التي تُعرَّف بها المسارات — مُزخرِف المتحكّم ومُزخرِفات أفعال HTTP، ومعها @ApiTags التي تمنحه تجميع الوحدات نفسه المستعمل عناوينَ هنا — فهو يصف دائماً البناء الجاري فعلاً، وهو صفحة تُتصفَّح وفيها زر تجربة على كل عملية، لا وثيقة تُقرأ. والمسارات والأفعال والوسوم فيه مكتملة؛ أما مخططات الوسائط والمتون فليست أدقّ من المُزخرِفات نفسها، ولا وجود بعدُ لأي تعليق @ApiProperty، ولذلك تبقى أشكال الحمولات المرسومة تحت كل وحدة أدناه هي المرجع لها.
مساران، وكلاهما مركَّب خارج البادئة /api/v1 (SWAGGER_PATH في apps/api/src/app.setup.ts). والرابطان أدناه يُحَلّان مقابل مضيف الواجهة الذي يُهيَّأ به موقع التوثيق هذا:
/api/docs— واجهةSwaggerنفسها./api/docs-json— وثيقةOpenAPIالخام، وتشتقّها@nestjs/swaggerمن مسار الواجهة فلا يمكن أن يصف الاثنان مخططين مختلفين. وهذه هي التي تُستورَد فيPostmanأوInsomnia، أو تُسلَّم لمولّد عملاء.
وهو في بيئة التطوير مُشغَّل دون أن يُطلَب. فالدالة apiDocsEnabled() تحترم ENABLE_API_DOCS حين تكون مضبوطة، وإلا ارتدّت إلى الشرط NODE_ENV !== "production"؛ فتُقدِّم واجهة التطوير المسارين دون أي تهيئة. والقيمة الصريحة تغلب في الاتجاهين: فالضبط ENABLE_API_DOCS=false يُطفئ التوثيق على جهاز التطوير أيضاً.
وفي أي بيئة منشورة لا يُقدَّم إلا حيث تُضبط ENABLE_API_DOCS=true، ولا يُقدَّم قط بلا بيانات دخول. والبوابة مقصودة (D-30)، ويذكر الملف apps/api/src/main.ts سببها في سطر واحد: المخطط خريطةٌ لسطح الهجوم. فجردٌ كاملٌ مقروء آلياً بكل مسار ووسيط وفحص دور يخدم المهاجم خدمةً لا يخدمها المتكامِل، إذ يكفي أن يُرسَل إليه الملف.
ولذلك فتشغيله يُشغِّل بيانات الدخول معه في الوقت نفسه. فالوسيط apiDocsAuth في الملف apps/api/src/app.setup.ts يطالب بمصادقة أساسية على البادئة /api/docs كلها — بما فيها المخطط الخام، إذ إن حماية الشطر المقروء وحده تمثيلٌ لا حماية — كما تجعل الدالة apiDocsCredentialProblem في الملف apps/api/src/common/env.ts ضبطَ ENABLE_API_DOCS=true دون API_DOCS_USER وAPI_DOCS_PASSWORD خطأً قاتلاً عند الإقلاع. فالتهيئة الناقصة تفشل بوصفها واجهةً ترفض الإقلاع، لا بوصفها مخططاً يقرؤه أي أحد. ووُضع الفحص في التطبيق لا في الوسيط العكسي عن قصد: فملف بيانات الدخول تحت /etc/nginx خفيٌّ عن المراجعة، وغائبٌ عن كل بيئة أخرى، ويضيع عند إعادة توليد إعداد المضيف.
وهو مُفعَّل في الإنتاج، فيردّ المساران 401 على الطلب المجهول و200 على الطلب المصحوب ببيانات الدخول. وتُسلَّم بيانات الدخول لكل جهة على حدة، لا تُنشَر هنا.
والمصادقة تجري بالكوكيز، فلا رمز يُلصَق في خانة. تصادق الواجهة بكوكيَّتين من نوع httpOnly هما nutqi_at (للوصول) وnutqi_rt (للتجديد)، ولهذا تُعلن تهيئة Swagger السطر addCookieAuth("nutqi_at") بدل مخطط الرمز الحامل. وهذا الإعلان يسمّي الكوكية في المخطط ويؤشّر العمليات التي تحتاجها؛ وليس موضعاً تُكتب فيه بيانات اعتماد، لأن الصفحة لا تستطيع ضبط ترويسة Cookie، ولأن كوكية httpOnly لا تُقرأ بالبرمجة أصلاً. والذي يجعل زر التجربة يعمل هو الجلسة التي يحملها المتصفح سلفاً: فالواجهة تُقدَّم من أصل الخادم نفسه، فتكون طلباتها على الأصل ذاته وترافقها الكوكية تلقائياً. أما متصفح بلا جلسة فيسجّل الدخول من الصفحة نفسها — يُنفَّذ POST /auth/login منها فتُضبط الكوكيز على ذلك الأصل، وتصير كل عملية بعده مصادَقاً عليها.
وحدة health#
GET /health(عام، بلا مصادقة) ← فحص حياة مع فحص قاعدة بيانات حقيقي. وهو خالٍ عمداً من أرقام الإصدارات وسلاسل الاتصال ونصوص الأخطاء؛ ويستهلكه الوسيط العكسي وفحص صحة الحاوية.
وحدة auth#
POST /auth/registerبحمولة{ role: PERSONAL_PATIENT|GUARDIAN|SPECIALIST|CENTER, firstName, lastName, email, phone, password, gender?, birthDate?, center? {nameAr, licenseNumber?, governorate?, city?, branchName?} }← تُنشئ مستخدماً (بالحالةPENDING_VERIFICATION)، ومعه مركز وأول فرع حين يكون الدورCENTERطبقاً لـG03؛ وتُرسل رمز تحقق للبريد والهاتف (المزوّد في بيئة التطوير هوconsole). تردّ 409 عند تكرار البريد أو رقم الهاتف.POST /auth/verify-otpبحمولة{ target, code, purpose: "verify" | "reset" }← عند التحقق: تُعلَّم الوسيلة كمُوثَّقة، وتتحول الحالة إلىACTIVE(للمريض وولي الأمر) أوPENDING_ACTIVATION(للأخصائي والمركز)؛ وتُضبط الكوكيز. وعند إعادة التعيين: تُعيد رمزresetTokenصالحاً لمرة واحدة.POST /auth/resend-otpبحمولة{ target, channel? }— محكومة بمهلة 60 ثانية (مطابقة للعدّاد التنازلي في الواجهة).POST /auth/loginبحمولة{ email, password }← تضبط الكوكيز؛ وكلمة السر الخطأ تزيد عدّاد المحاولات؛ وعند القفل تردّ 423 معlockedUntilطبقاً لـG15.POST /auth/forgotبحمولة{ email, phone }← رمز تحقق بغرضreset.POST /auth/reset-passwordبحمولة{ resetToken, password }.POST /auth/refreshوPOST /auth/logout.GET /auth/me← بيانات المستخدم + ملخص الملف الخاص بدوره + نسبة اكتمال التفعيل (وهي ما يحرّك حلقة البوابة).PATCH /auth/passwordبحمولة{ current, next }(من الإعدادات).
وحدة users#
PATCH /users/me(الأسماء بالعربية والإنجليزية،nationalId، تاريخ الميلاد، النوع، الصورة الشخصية، اللغة، وحقول العنوان).PUT /users/me/languagesبحمولة[{language, level}](اللغات المحكية،G13-b).GET/PUT /users/me/notification-channelsبحمولة[{channel, target, enabled}]؛ وPOST /users/me/notification-channels/:channel/verify(مسار رمز التحقق).
وحدة patients (بوابة ولي الأمر)#
GET /patients(الخاصة بي) /POST /patients(إضافة طفل،G09) /GET|PATCH /patients/:id.POST /patients/:id/documents(رفع متعدد الأجزاء، النوعDOC|VIDEO) /GET /patients/:id/documents/DELETE …/:docId.- التشخيص:
GET /forms/diagnosis-template← القالب النظامي؛ وPOST /patients/:id/diagnosis-response(تُنشئ الاستجابة أو تحدّثها عبر واجهة الإجابات الموصوفة أدناه).
وحدة specialists#
GET /specialists— الدليل العام (G12): المرشِّحاتqوgovernorateوspecialtyوsessionTypeوpriceMin/Maxوrating؛ ولا يظهر إلا من كان بالحالةACTIVE.GET /specialists/:id/profile← حمولة النظرة العامة (الإحصاءات: الخط الزمني للحجوزات، وتوزيع الأنواع، وأعلى البرامج — وكلها محسوبة)، والبيانات (الشهادات والتدريبات والفيديوهات)، والعيادات، وملخص التقييمات.GET /specialists/:id/schedule?weekStart=← شبكة الجلسات مع الأسعار؛ وGET /specialists/:id/slots?date=&clinicId?=← المواعيد المتاحة بفترات نصف ساعة (ساعات العمل − الحجوزات − أيام الراحة) طبقاً لـD-09.- مقصورة على الحساب نفسه:
GET/PUT /specialists/me/profile، وPOST/PATCH/DELETE /specialists/me/work-info، و…/certificates، و…/videos، و…/clinics، وPUT /specialists/me/schedule(كتل أيام الأسبوع)، وPOST /specialists/me/days-off. PUT /specialists/me/booking-settingsبحمولة{ availableForWork, acceptsOnline, acceptsOffline, acceptsConsultation }.GET/POST/DELETE /specialists/me/blocklist.GET /specialists/me/stats?period=all|year|month|week|day←{ dailyAvgCases, totalCases, earningsCents, pending: bool }.
وحدة bookings#
POST /bookingsبحمولة{ patientId, specialistId, clinicId?|type ONLINE, sessionType, date, startTime }← تُنشئ الحجز بالحالةPENDING؛ وتتحقق من أن الميعاد شاغر، وأن الأخصائي مفعَّل ومتاح، وأن الحاجز غير محظور؛ ويُحتسب السعر في الخادم. وتردّ 409 إذا كان الميعاد محجوزاً.GET /bookingsقائمة محدودة بنطاق الدور مع مرشِّحات (statusوtypeوqوdateRange) بالإضافة إلى?export=csv.PATCH /bookings/:id/statusبحمولة{ status, meetingUrl?, newDate?/newTime? for POSTPONED }— الانتقالات المسموح بها محكومة بآلة الحالات (المواصفةdocs-content/02§5)؛ ويجوز لولي الأمر إلغاء حجزه هو وهو بالحالةPENDING/WAITING.GET /bookings/upcoming← الحجز التالي لعرضه في الشريط طبقاً لـE10.
وحدة sessions#
POST /sessions(من حجز أو من حالة خاصة) /PATCH /sessions/:idبحمولة{ progressPercent, evaluation, notes }.GET /patients/:id/sessions، وGET /special-cases/:id/sessions.
وحدة special-cases (للأخصائي)#
- عمليات إنشاء وقراءة وتعديل وحذف على
/special-cases؛ والمرفقات على/special-cases/:id/attachments(المرحلةBEFORE|AFTER)؛ والملاحظات لها العمليات نفسها (حذف ناعم مع نافذة تراجع،G18).
وحدة forms (محرك الخطط والمقاييس)#
- القوالب:
GET/POST /forms/templates(الخاصة بي)، وGET/PATCH/DELETE /forms/templates/:id(مع حمولة متداخلة للصفحات والأسئلة، ومُدارة بالإصدارات). - الإسناد:
POST /forms/assignmentsبحمولة{ templateId, patientId|specialCaseId, dueDate? }← إشعار إلى ولي الأمر. GET /forms/assignments?role=guardian|specialist&status=← بيانات البطاقات (لم يتم / تم).- الإجابة:
GET /forms/assignments/:id/response(أو إنشاؤها)، وPUT /forms/responses/:id/answers/:questionIdبحمولة{ value }— حفظ تلقائي بالإدراج أو التحديث طبقاً لـE01، وPOST /forms/responses/:id/submit← يتحقق من الحقول المطلوبة ← يصبح الإسنادANSWERED. - النتائج:
GET /forms/assignments/:id/result(عرض للقراءة فقط للسؤال والإجابة).
وحدة reviews#
POST /reviewsبحمولة{ specialistId, bookingId?, stars, text, kind }(يشترط أن يكون لصاحب التقييم حجز بالحالةDONEمع الأخصائي حين يكون النوعSESSION).GET /specialists/:id/reviews?kind=&period=؛ وPATCH /reviews/:id/like، وPATCH /reviews/:id/reply(للأخصائي)، وPOST /reviews/:id/reportطبقاً لـG17.
وحدة payments#
GET /payments/mine(سجل مدفوعات ولي الأمر،G01) ← الصفوف مع الإجماليات.POST /bookings/:id/paymentبحمولة{ method, status }(تسجّله السكرتارية أو الأخصائي؛ ويُنشئ معاملة محفظة من نوعEARNINGعند الحالةPAID).GET /wallet(للأخصائي) ←{ balanceCents, withdrawnCents }؛ وGET /wallet/transactions.POST /wallet/withdrawalsبحمولة{ amountCents, method, target }طبقاً لـG19؛ وGET /wallet/withdrawals.
وحدة centers#
GET/PATCH /centers/me(للمالك والمدير)؛ والفروع لها العمليات الكاملة على/centers/me/branches.- الموظفون:
GET/POST /centers/me/staff(إنشاء مستخدم موظف مع الدور والفرع والراتب)، وPATCH/DELETE /centers/me/staff/:id. - مرضى المركز:
GET /centers/me/patients(مع تمرير طلبات الملفات الشخصية).
وحدة hr (للمركز)#
POST /hr/attendance/clock-in|clock-out(للموظف نفسه، بدورCENTER_SPECIALIST/SECRETARY) — وbranchIdاختياري طبقاً لـG20.GET /hr/attendance?staffId?&range(المالك والمدير يريان الجميع؛ والموظف يرى سجلّه هو).- عمليات شبه كاملة على
/hr/absences، و/hr/overtime، و/hr/penalties(معdeductionCents)، وهي للمالك والمدير فقط. - الطلبات:
POST /hr/requests(للموظف)، وGET /hr/requests(محدودة بالنطاق)، وPATCH /hr/requests/:id/decisionبحمولة{ status: APPROVED|REJECTED }(للمالك والمدير) ← إشعار.
وحدة jobs#
- للمركز:
GET/POST /jobs/postings، وPATCH /jobs/postings/:id(مسودة ← منشورة ← مغلقة)، والطلبات:GET /jobs/postings/:id/applications، وPATCH /jobs/applications/:id/decision← عند الحالةACCEPTEDتُعرض حمولة إنشاء سجلCenterStaff. - للأخصائي:
GET /jobs/market(المنشورة، مع مرشِّحات،G08)، وPOST /jobs/postings/:id/apply، وGET /jobs/applications/mine.
وحدة notifications#
GET /notifications?unread=، وPATCH /notifications/:id/read، وPATCH /notifications/read-all.GET /notifications/stream— بتقنيةSSEطبقاً لـE13.- خدمة إطلاق الأحداث تستعملها بقية الوحدات؛ وهي تُرسل دائماً عبر القناة
IN_APPإضافةً إلى القنوات المفعَّلة والمُوثَّقة (البريد عبرconsoleأوSMTPتطويري؛ وواتساب وتليجرام واجهتان صوريتان تسجّلان الحمولات فقط،D-17).
وحدة files#
POST /filesرفع متعدد الأجزاء (يتطلب المصادقة) ←StoredFile؛ وGET /files/:id(الصلاحية بحسب الملكية أو الارتباط)؛ مع حدود للحجم والنوع (الصور 5 ميجابايت، والمستندات 10 ميجابايت، والفيديو 100 ميجابايت).
وحدة admin (عبر واجهة API فقط في الإصدار الأول)#
GET /admin/activations(الأخصائيون والمراكز المعلَّقون)، وPATCH /admin/activations/:userIdبحمولة{ approve|reject }.GET /admin/withdrawals، وPATCH /admin/withdrawals/:idبحمولة{ TRANSFERRED|REJECTED }.GET /admin/reported-reviews.
الأحداث ← الإشعارات (الحد الأدنى)#
booking.created (← الأخصائي/السكرتارية)، و booking.status_changed (← ولي الأمر)، و assignment.created (← ولي الأمر)، و assignment.answered (← الأخصائي)، و application.decided (← الأخصائي)، و hr.request.decided (← الموظف)، و withdrawal.processed (← الأخصائي)، و activation.decided (← المستخدم).