feat: P1 — بوابات دفع حقيقية (PayMob) + إغلاق ثغرة webhook حرجة

الأهم أولاً: `PaymentsService.webhook()` كان يقبل أي جسم `{ payment_id }`
بلا أي تحقّق توقيع — من يعرف معرّف دفعة معلَّقة كان يستطيع تحويلها «ناجحة»
ويشحن رصيداً من عدم (محفظة راكب أو رصيد سائق تشغيلي). الآن كل تغيير حالة
محروس بـ`adapter.verifyWebhook(headers, payload, tenant)`، ولا شيء يُقرأ من
الحمولة قبل ذلك كقرار ثقة — قراءة المرجع لتحديد المستأجر ليست قراراً.

البنية (docs/07 · docs/24 — P1):
- `PaymentAdapter`: charge() يعيد instant (كاش) · redirect (بوابة API حقيقية)
  · invoice (بلا API، تسوية عبر P2).
- PayMob (مصر): تسلسل auth→order→payment_key→iframe حقيقي عبر fetch،
  وتحقّق HMAC-SHA512 على تسلسل حقول ثابت (بروتوكول PayMob الرسمي بالضبط)
  بمقارنة ثابتة الزمن. مفاتيح كل مستأجر مستقلة — حساب تاجر خاص به.
- كليق/شام كاش/MTN/سيرياتيل/زين كاش: محوّل مشترك واحد لأن سلوكها متطابق
  فعلياً في سيرو (`create_*_invoice.php` تُنشئ فاتورة فقط، لا نداء بوابة
  حيّاً) — مرجع + حساب استلام معروض، والتسوية عبر رسالة SMS لا webhook.
  MTN/سيرياتيل الحقيقيَّين (توكن+OTP) موثَّقان كبند مفتوح: لا نبني تكاملاً
  لا نملك اعتماداً حيّاً للتحقّق منه.
- `PATCH /payments/settings` لأدمن المستأجر: مفاتيح PayMob · حسابات
  الاستلام · سرّ webhook الرسائل — الاستجابة لا تُعيد الأسرار.

تنظيف: إزالة الإشارات المتبقّية لحاوية `martin` من docs/07 (أُزيلت فعلياً
سابقاً)، وتحديث هيكل الكود الموثَّق ليطابق ما هو مبنيّ فعلاً.

25 اختباراً جديداً (226 إجمالاً) — منها توقيع PayMob محسوب فعلياً ومُتحقَّق،
وتلاعبٌ بالحمولة بعد التوقيع يُرفض، وسبع حالات تثبت إغلاق ثغرة الـwebhook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-18 15:50:05 +03:00
co-authored by Claude Sonnet 5
parent da035e46a4
commit d4ac38ca87
16 changed files with 852 additions and 42 deletions
+21 -18
View File
@@ -2,22 +2,21 @@
> المبدأ: كل تكامل خارجي = **محوّل (Adapter)** خلف واجهة موحّدة، يُفعَّل من [country pack](06-tenant-model.md). إضافة مزوّد جديد = ملف adapter واحد، لا مساس بالمنطق.
## 1. الدفع (Payment Adapters)
## 1. الدفع (Payment Adapters) — منجَز (docs/24 — P1)
```
interface PaymentAdapter {
charge(ctx, amount, currency): PaymentResult
refund(ctx, txId): RefundResult
status(txId): PaymentStatus
charge(ctx, tenant): Promise<ChargeResult> // instant | redirect | invoice
verifyWebhook(headers, rawBody, tenant): boolean
parseWebhook(payload): WebhookResult | null
}
```
| المزوّد | الأولوية | السوق |
|---------|---------|-------|
| Cash | اليوم الأول | الكل |
| CliQ | P2 | الأردن |
| ZainCash | P2 | الأردن |
| Syriatel Cash / MTN Cash | P2 | سوريا |
| Binance Pay | P2 (عمل جاهز في مستودعك) | عابر |
| المزوّد | النمط الفعلي | السوق |
|---------|-------------|-------|
| Cash | `instant` | الكل |
| PayMob | `redirect` — **API حقيقية** (auth→order→payment_key→iframe) + HMAC | مصر |
| CliQ · شام كاش · MTN · سيرياتيل · زين كاش | `invoice` — **بلا API فعلية**، مرجع + تسوية عبر SMS (P2) | الأردن/سوريا |
- التسويات (settlements) للسائقين والمستأجر عبر BullMQ + سجل `payouts`.
- فلسفة المال (إيراد مقابل أمانة) ومحفظتا المستأجر: [24-tenant-wallet-revenue](24-tenant-wallet-revenue.md).
## 2. OTP و SMS
- **ذاتي أولاً** (أصل موجود من مشروعك) + **مزوّد احتياطي لكل دولة**.
@@ -42,18 +41,22 @@ interface RegulatorAdapter {
- **Webhooks عامة** للمستأجرين المتقدمين (أسطول+/سيادة): نظام محاسبة، ERP أسطول... — توقيع HMAC + إعادة محاولة.
## 5. الخرائط (انطلق) — الخندق التنافسي
- البلاطات عبر **Martin** (HTTPS)، الترميز/التوجيه/snapping من واجهات انطلق.
- البلاطات من **خوادم انطلق نفسها مباشرة** — لا نستضيفها (قرار مصحَّح 2026-07-18، docs/25 §1: أُزيلت حاوية `martin` التي كانت مخطَّطة سابقاً؛ انطلق منصّة قائمة بذاتها، دورنا طلبٌ وردّ).
- الترميز/التوجيه/snapping من واجهات انطلق عبر سيرفرنا (يحمي المفتاح ويكيّش).
- **صفر اعتماد على Google** في القلب.
- **طبقة تجريد** في الباك إند والموبايل تسمح بتبديل مزوّد البلاطات لأي مستأجر خارج تغطية انطلق.
- سطر البيع: «خرائط غير محدودة مشمولة — بلا فاتورة للأبد».
## هيكل الكود
## هيكل الكود (الفعلي)
```
backend/src/integrations/
├── payments/ { cash, cliq, zaincash, syriatel, mtn, binance }.adapter.ts
├── sms/ { self, twilio, local-jo, local-sy }.adapter.ts
├── regulator/ { ltrc, generic }.adapter.ts
└── registry.ts # يربط اسم المزوّد (من country pack) بالمحوّل
├── payments/
│ ├── payment-adapter.interface.ts
│ ├── cash.adapter.ts
│ ├── paymob.adapter.ts # API حقيقية + HMAC
│ ├── invoice.adapter.ts # cliq/shamcash/mtn/syriatel/zaincash — بلا API
│ └── payment-gateway.registry.ts
├── gemini/ # رؤية: وثائق + وجه + تحليل رسائل التحويل (P2)
└── otp/ # nabeh · kazumi + التوجيه حسب country pack
```
← السابق: [06-tenant-model](06-tenant-model.md) · التالي: [08-data-model](08-data-model.md)