# WALLET.md — تفصيل نظام التسجيل والمحفظة (SMM Panel) > ملف مكمّل لـ `AGENTS.md`. يركّز حصراً على جزئين حسّاسين: **نظام تسجيل الدخول/الحساب** > و**صحة المحفظة (Wallet Integrity)** — أي ضمان أن كل ريال/دولار يدخل أو يخرج من رصيد > المستخدم محسوب صح، موثّق، وغير قابل للتلاعب أو التكرار. --- ## 1. نظام الحساب (Auth) بالتفصيل ### 1.1 التسجيل (Register) ``` POST /auth/register Body: { username, email, password } ``` - تحقق من صيغة الإيميل، وأن `username` و `email` غير مستخدمين (UNIQUE constraint + تحقق على مستوى التطبيق). - كلمة المرور: حد أدنى 8 خانات، تُشفّر فوراً بـ `bcrypt` (salt rounds ≥ 10) — **لا تُخزَّن ولا تُطبع باللوق أبداً**. - عند الإنشاء: `balance = 0.0000` صراحةً (لا تترك القيمة الافتراضية ضمنية في الكود). - (اختياري لكن يُفضّل): إرسال إيميل تفعيل قبل السماح بالشحن أو الشراء، لتقليل الحسابات الوهمية. ### 1.2 الدخول (Login) ``` POST /auth/login Body: { email, password } ``` - قارن الباسورد بـ `bcrypt.compare`. - عند النجاح: أصدر JWT يحوي `{ user_id, role }` بمدة صلاحية قصيرة (مثلاً 15 دقيقة) + **refresh token** طويل مخزّن كـ httpOnly cookie أو في جدول `refresh_tokens` (قابل للإبطال عند تسجيل الخروج/تغيير الباسورد). - **Rate limiting إلزامي**: مثلاً 5 محاولات فاشلة / 15 دقيقة لكل IP + لكل حساب، ثم قفل مؤقت. هذا يمنع محاولات تخمين الباسورد للوصول لرصيد أحد. - لا ترجّع أبداً رسالة تفرّق بين "الإيميل غير موجود" و"الباسورد غلط" — رسالة موحدة لمنع تعداد الحسابات (user enumeration). ### 1.3 الجلسة والصلاحيات - Middleware `auth`: يتحقق من JWT ويحقن `req.user`. - Middleware `requireActive`: يرفض أي عملية مالية (شحن/شراء) إذا `status = banned`. - Middleware `admin`: يتحقق `role = admin` لكل مسار إداري. - عند أي عملية حساسة على المحفظة، تحقق من هوية المستخدم من الـ JWT فقط — **لا تثق أبداً بـ `user_id` قادم من body/query**. ### 1.4 نسيان كلمة المرور - توليد token عشوائي (32+ byte) + صلاحية قصيرة (15-30 دقيقة) + تخزينه مُشفّر (hash) في DB. - عند إعادة التعيين: أبطل كل الـ refresh tokens القديمة لهذا المستخدم (يقفل أي جلسة مسروقة). --- ## 2. صحة المحفظة (Wallet Integrity) — الجزء الأهم الهدف: **رصيد المستخدم = مجموع كل حركاته في `transactions`، دائماً، بدون استثناء.** إذا صار فرق بين `users.balance` ومجموع `transactions`، فهذا يعني وجود باغ خطير أو تلاعب. ### 2.1 القواعد الذهبية 1. **كل تغيير على `balance` يمر عبر خدمة واحدة فقط**: `wallet.service.js` — ممنوع أي كود آخر (controller أو route) يعدّل `balance` مباشرة بـ SQL. 2. **كل عملية = DB Transaction واحدة (atomic)** تشمل: قفل الصف، التحقق، التعديل، وتسجيل الحركة في `transactions` — الأربعة سوا أو ولا وحدة. 3. **استخدم `SELECT ... FOR UPDATE`** على صف المستخدم لمنع Race Condition (طلبين بنفس اللحظة يشتروا بنفس الرصيد). 4. **DECIMAL دائماً، لا FLOAT أبداً** — الأخطاء العشرية في FLOAT تسبب فروقات مالية حقيقية مع الوقت. 5. **لا رصيد سالب أبداً**: تحقق `balance >= amount` **داخل نفس الـ Transaction بعد القفل**، وليس قبل. 6. **كل حركة توثّق `balance_after`** — يعطيك سجل تدقيق كامل تقدر تعيد بناء الرصيد منه في أي لحظة تاريخية. 7. **Idempotency**: أي عملية قابلة لإعادة الإرسال (retry من الشبكة، دبل كليك، webhook مكرر) لازم يكون إلها مفتاح فريد يمنع تكرارها. ### 2.2 مثال Pseudo-code لخصم رصيد (شراء خدمة) ```js async function chargeForOrder(userId, amount, orderRef) { return db.transaction(async (trx) => { // 1. قفل صف المستخدم const user = await trx('users').where({ id: userId }).forUpdate().first(); // 2. تحقق من الحالة والرصيد داخل القفل if (user.status !== 'active') throw new AppError('الحساب موقوف'); if (Number(user.balance) < Number(amount)) throw new InsufficientBalanceError(); const newBalance = round4(Number(user.balance) - Number(amount)); // 3. عدّل الرصيد await trx('users').where({ id: userId }).update({ balance: newBalance }); // 4. سجّل الحركة (idempotency عبر reference فريد) await trx('transactions').insert({ user_id: userId, type: 'order', amount: -amount, balance_after: newBalance, reference: orderRef, // مثال: "order:1234" note: 'خصم مقابل طلب خدمة', }); return newBalance; }); // commit تلقائي، أو rollback كامل عند أي خطأ } ``` - إذا فشل إرسال الطلب لمزوّد الخدمة **بعد** نجاح الخصم: نفّذ عملية عكسية (`refund`) بنفس الآلية أعلاه، لا تعدّل الرصيد يدوياً. - ضع `UNIQUE constraint` على `transactions.reference` (أو composite `user_id + reference + type`) — هذا يمنع تسجيل نفس الحركة مرتين حتى لو صار retry. ### 2.3 الشحن اليدوي — كيف تتأكد إن المصاري "صحيحة" فعلاً هذا أهم نقطة طلبتها، فهاي التفاصيل: **أ) منع تكرار نفس إثبات التحويل:** - `deposits.tx_ref` لازم `UNIQUE` — يمنع مستخدم يقدّم نفس رقم التحويل/الهاش مرتين (أو مستخدمين مختلفين يستخدموا نفس الإثبات). **ب) الموافقة تتم فقط من الأدمن (لا auto-approve إلا لو عندك تكامل حقيقي مع بوابة/بلوكتشين):** ``` PATCH /admin/deposits/:id { action: 'approve' | 'reject', note } ``` - عند `approve`، داخل DB Transaction واحدة: 1. قفل صف `deposits` (`FOR UPDATE`) وتأكد `status = pending` (يمنع أدمن ثاني يوافق مرتين بنفس اللحظة). 2. قفل صف `users`. 3. `balance += deposit.amount`. 4. أنشئ `transaction` بـ `type=deposit، reference = "deposit:" + deposit.id`. 5. حدّث `deposits.status = approved`, `reviewed_by = admin.id`. 6. Commit واحد للكل. - **لا تسمح بتعديل `amount` بعد الإنشاء** — إذا المبلغ غلط، الأدمن يرفض ويطلب من المستخدم تقديم طلب جديد. هذا يحافظ على أن `deposits.amount` = المصدر الوحيد للحقيقة. **ج) لو رح تضيف تحقق تلقائي لاحقاً (USDT مثلاً):** - تحقق من الـ tx hash على البلوكتشين فعلياً (عدد التأكيدات، العنوان المستلم، المبلغ يطابق تماماً) قبل أي auto-approve. - خزّن نتيجة التحقق (raw response) في عمود إضافي للتدقيق. - لا تثق بأي مبلغ يرسله المستخدم في الفورم — تحقق منه مقابل البلوكتشين/بوابة الدفع نفسها. **د) تعديل رصيد يدوي من الأدمن (`PATCH /admin/users/:id`):** - يمر بنفس آلية `wallet.service.js` (قفل + تسجيل transaction بـ `note` يوضح السبب و`reference` يربط بمن نفّذ التعديل) — **ممنوع UPDATE مباشر على عمود `balance`**. ### 2.4 التسوية الدورية (Reconciliation) مهمة cron (يومية مثلاً) تتحقق: ```sql SELECT user_id, SUM(amount) AS calculated FROM transactions GROUP BY user_id ``` وتقارن الناتج مع `users.balance` لكل مستخدم. أي فرق = تنبيه فوري للأدمن (لا تصحيح تلقائي — تحقيق يدوي أولاً). ### 2.5 حالات حافة لازم تُختبر | الحالة | السلوك المطلوب | |---|---| | طلبين شراء بنفس اللحظة من نفس المستخدم | الثاني يفشل أو ينتظر القفل، لا يمرّان معاً إذا الرصيد يكفي لواحد فقط | | فشل الشبكة بعد الخصم وقبل استلام رد المزوّد | job دوري يتحقق من حالة الطلب عند المزوّد، وإن لم يُنشأ فعلياً يرد المبلغ | | Retry تلقائي من الفرونت (دبل كليك على "شراء") | idempotency key يمنع الخصم مرتين لنفس الطلب | | أدمن يوافق على نفس طلب الشحن مرتين (تبويبين مفتوحين) | القفل (`FOR UPDATE`) + شرط `status=pending` يمنع التكرار | | مستخدم يقدّم رقم تحويل مستخدم سابقاً | `UNIQUE` على `tx_ref` يرفضه فوراً | | رصيد يوشك يصير سالب بسبب سباق (race) | التحقق من الرصيد يتم بعد القفل مباشرة، ضمن نفس الـ transaction | --- ## 3. جدول Idempotency (اختياري لكن مُستحسن) لأي عملية POST حساسة (شراء، شحن)، خلي الفرونت يرسل `Idempotency-Key` فريد لكل محاولة: | العمود | النوع | |---|---| | key | VARCHAR(64) PK | | user_id | BIGINT | | endpoint | VARCHAR(100) | | response_snapshot | JSON | | created_at | TIMESTAMP | إذا وصل نفس المفتاح مرة ثانية، رجّع نفس النتيجة المخزّنة بدل تنفيذ العملية من جديد. --- ## 4. اختبارات إلزامية لمنطق المحفظة - شراء بمبلغ أكبر من الرصيد → يُرفض، لا يتغيّر الرصيد. - شراءين متزامنين (parallel) بمبلغ كل واحد = نص الرصيد → واحد فقط ينجح. - فشل إرسال الطلب للمزوّد بعد الخصم → رصيد المستخدم يرجع كما كان (refund تلقائي). - موافقة أدمن على شحن مرتين بالتوازي → إضافة واحدة فقط للرصيد. - تقديم `tx_ref` مكرر → رفض فوري بدون لمس الرصيد. - بعد N عملية عشوائية (شحن/خصم/استرجاع)، `SUM(transactions.amount) == users.balance` دائماً. --- ## 5. ملخص مسارات الحساب والمحفظة ``` POST /auth/register POST /auth/login POST /auth/logout → يُبطل الـ refresh token POST /auth/forgot-password POST /auth/reset-password GET /me → الرصيد + بيانات الحساب GET /transactions → كشف حساب كامل (مع balance_after لكل حركة) POST /deposits → تقديم طلب شحن (amount, method, tx_ref) GET /deposits → طلبات الشحن الخاصة بي وحالتها PATCH /admin/deposits/:id → موافقة/رفض (atomic كما بالقسم 2.3) PATCH /admin/users/:id → تعديل رصيد يدوي (يمر بـ wallet.service.js حصراً) ``` --- ## 6. ملاحظة للوكيل البرمجي عند تنفيذ أي كود يلمس `balance`، اسأل نفسك دائماً: *"هل هذا داخل DB transaction مقفول بـ FOR UPDATE، وهل يسجّل صف في transactions؟"* — إذا الجواب لا لأي منهما، الكود غير جاهز للإنتاج بغض النظر عن أي شيء آخر.