الحجوزات والمدفوعات
كيف يُنشأ الحجز ويُدفع ويُؤكد ويُلغى: البطاقة عبر Stripe، وPayPal، والدفع عند الوصول، والمحاكي المرفق، وwebhook التي تؤكد الدفع، والاستردادات، والخطوات التي يتخذها المضيف في لوحة التحكم.
لحزمة الحزمة الكاملة
ثلاث طرق للدفع
| الطريقة | متى تُعرض | أين يدفع الضيف |
|---|---|---|
| البطاقة | عبر Stripe حين يُضبط STRIPE_SECRET_KEY؛ وعبر المحاكي المرفق ما دام لا معالج مضبوط إطلاقًا | صفحة Checkout المستضافة من Stripe، فلا يصل رقم بطاقة إلى هذا التطبيق؛ ومع المحاكي نموذج بطاقة في إتمام الطلب |
| PayPal | عبر PayPal حين يُضبط PAYPAL_CLIENT_ID وPAYPAL_CLIENT_SECRET؛ وعبر المحاكي ما دام لا معالج مضبوط إطلاقًا | صفحة الموافقة في PayPal |
| الدفع عند الوصول | ما دام إعداد allow_pay_on_arrival مفعّلًا (الإعدادات ثم المدفوعات) | لا شيء عبر الإنترنت: يحصّله المضيف عند الباب |
| المضبوط على واجهة API | المعروض عبر الإنترنت | المرفوض |
|---|---|---|
| لا Stripe ولا PayPal | البطاقة وPayPal، وكلاهما يسوّيه المحاكي | لا شيء |
| Stripe فقط | البطاقة، عبر Stripe | PayPal |
| PayPal فقط | PayPal، عبر PayPal | البطاقة |
| كلاهما | البطاقة عبر Stripe، وPayPal عبر PayPal | لا شيء |
يُعرض الدفع عند الوصول بجانب أي من هذه ما دام إعداده مفعّلًا، ويُرفض ما دام متوقفًا. يقرأ إتمام الطلب القائمة من GET /api/payments/methods، فلا يعرض أبدًا طريقة لا تقبلها واجهة API، ولا تطلب الطريقة المستضافة بيانات بطاقة في الموقع. والحجز المرسل بطريقة مرفوضة يُجاب بـ «طريقة الدفع هذه غير متاحة. اختر طريقة دفع أخرى.» قبل كتابة أي شيء، فلا يحتفظ بأي ليالٍ. ويذهب المال إلى حساب Stripe أو PayPal الخاص بك؛ والمنتج لا يحتفظ به أبدًا.
المحاكي المرفق
التثبيت الجديد بلا معالج، فيسوّي المحاكي كل حجز بالبطاقة أو PayPal فورًا دون الاتصال بأحد. يمكن تجربة التدفق كله بالنقر على حاسوب محمول بلا مفاتيح ولا حساب تاجر ولا شبكة، والحجز والتأكيد والتقويم وأرقام الإيرادات صفوف حقيقية يكتبها التدفق الحقيقي. ويُرفض رقم بطاقة ينتهي بـ 0 ليظهر مسار الفشل.
أثناء تشغيله يقول نموذج البطاقة في إتمام الطلب «وضع الاختبار: لا تخصم أي مبالغ من البطاقة.»، ويجيب GET /api/payments/methods بـ simulated: true، وتكتب واجهة API سطرًا واحدًا في سجلها عند البدء:
[Payments] Payment simulator active: card and PayPal bookings are confirmed without payment until a processor is configured (STRIPE_SECRET_KEY, or PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET).لا يأخذ أي مال
الحجز الذي يؤكده المحاكي لم يُدفع أبدًا، لذلك يعمل فقط ما دام Stripe وPayPal كلاهما غير مضبوط. اضبط المعالج الذي سيدفع به ضيوفك قبل أن يُفتح الموقع لهم: ومن حينها يتوقف المحاكي، ولا يعرض إتمام الطلب إلا الطرق التي تقبلها معالجاتك، مع الدفع عند الوصول ما دام مفعّلًا. ويقول تبويب المدفوعات في إعدادات لوحة التحكم متى يُستخدم المحاكي.
دورة حياة الحجز
| الحالة | المعنى | التالي |
|---|---|---|
| قيد الانتظار | أُجري في الموقع، وينتظر دفعه أو المضيف | مؤكد أو ملغى |
| مؤكد | مدفوع، أو قبله المضيف للدفع عند الوصول | تم الوصول أو ملغى |
| تم الوصول | وصل الضيف. يُعلَّم هنا الحجز بالدفع عند الوصول مدفوعًا | مكتمل أو ملغى (إقامة قُطعت) |
| مكتملة | انتهت الإقامة، ويمكن للضيف تقييمها | لا شيء |
| ملغى | من الضيف، أو من المضيف، أو لأن احتجازًا غير مدفوع انتهت مهلته | لا شيء |
- حجز البطاقة أو PayPal يؤكده دفعه، لا اليد أبدًا: يُرفض «تأكيد» المضيف بالرسالة «لا يمكن تأكيد هذا الحجز قبل وصول الدفعة.» إلى أن يُدفع. أما الحجز بالدفع عند الوصول فينتظر المضيف ليؤكده.
- ينقل المضيف الحجز إلى الأمام في صفحته في لوحة التحكم بـ «تأكيد» و«تسجيل الوصول» و«إكمال» و«إلغاء»، ويحتفظ بملاحظة داخلية لا يراها الضيف. والخطوة التي لا يستطيع الحجز اتخاذها الآن تظهر معطّلة مع سببها.
- يتحرك الحجز إلى الأمام فقط، والمكتمل والملغى نهائيان.
- لكل حجز مرجع يُعطى بالترتيب (
B0001وB0002وهكذا)، ويحتفظ بسعر الليلة ورسوم التنظيف والإجمالي التي أُجري بها. - يخبر جرس لوحة التحكم كل مسؤول يستطيع عرض الحجوزات حين يُدفع حجز (بواسطة webhook أو التقاط PayPal أو المحاكي)، وحين ينتظر حجز بالدفع عند الوصول المضيف، وحين يُلغى حجز، ويقول هل أنهاه الضيف أم الفريق أم احتجاز غير مدفوع انتهت مهلته.
ما الذي يؤكد الدفع
webhook المعالج، لا عودة الضيف إلى الموقع. يعود الضيف المرسل إلى Stripe أو PayPal إلى صفحة حجزه، على SITE_URL، التي تقول إن الدفع قيد التأكيد إلى أن يصل webhook. والدفع الذي يفشل يترك الحجز قيد الانتظار، فيمكن للضيف المحاولة مرة أخرى حتى تنتهي مهلة الاحتجاز البالغة 30 دقيقة.
- يحتاج PayPal إلى خطوة واحدة أكثر من Stripe: الموافقة تترك الطلب موافقًا عليه ولا تأخذ شيئًا، فتلتقطه صفحة العودة، وتسوّي نتيجة الالتقاط نفسها أو webhook الحجز.
- الدفع الذي يصل متأخرًا لحجز ملغى أصلًا يُسجَّل مدفوعًا ويُترك للمضيف ليستردّه: فقد تكون الليالي صارت لشخص آخر.
- يعيد كل مزود محاولة webhook الخاصة به، والحدث الذي يصل مرتين يؤكد الحجز مرة واحدة.
إعداد Stripe
في .env الخاص بواجهة API، بمفتاح تجريبي أثناء التجربة (مفتاح sk_live_ يحرّك مالًا حقيقيًا، وتقول ذلك سجلات واجهة API عند البدء):
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...| الحدث | وظيفته |
|---|---|
checkout.session.completed | يؤكد الحجز. هو الحدث الوحيد الذي يفعل ذلك، لأنه الوحيد الذي يعني أن الجلسة دُفعت |
payment_intent.payment_failed | يعلّم الدفع فاشلًا ويترك الحجز قيد الانتظار، فيمكن للضيف الدفع مرة أخرى |
charge.refunded | يعلّم الدفع مستردًّا |
charge.dispute.created | نزاع على الدفع، يُعامل كاسترداد |
على الخادم، أنشئ نقطة webhook في Stripe على العنوان العام لواجهة API متبوعًا بـ /api/webhooks/payments/stripe، لهذه الأحداث الأربعة، وضع سر توقيعها في STRIPE_WEBHOOK_SECRET. محليًا، مرّر الأحداث بواسطة Stripe CLI وضع السر الذي يطبعه في .env:
stripe listen --forward-to localhost:8000/api/webhooks/payments/stripeثم أتمم عملية دفع تجريبية حقيقية في المتصفح بالبطاقة 4242 4242 4242 4242. لا فائدة هنا من stripe trigger checkout.session.completed: فالحدث المُطلَق لا يحمل مرجع حجز، فيُتجاهل. وبدون STRIPE_WEBHOOK_SECRET يُرفض كل webhook من Stripe ولا يُسوّى حجز بالبطاقة أبدًا.
اضبط PayPal
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_WEBHOOK_ID=...
PAYPAL_ENV=sandbox- يُعرض PayPal حين يُضبط معرّف العميل والسر معًا. و
PAYPAL_ENVهوsandboxما لم تضبطه علىlive، فلا يأخذ نشر مضبوط خطأً أي مال حقيقي. - اشترك بـ webhook في
PAYMENT.CAPTURE.COMPLETED(يؤكد الحجز) وPAYMENT.CAPTURE.DENIED(فشل الدفع، ويبقى الحجز قيد الانتظار) وPAYMENT.CAPTURE.REFUNDEDوPAYMENT.CAPTURE.REVERSED(يُعلَّم مستردًّا) على عنوان واجهة API متبوعًا بـ/api/webhooks/payments/paypal، وضع معرّف الاشتراك فيPAYPAL_WEBHOOK_ID: فهو الذي يتحقق من توقيع PayPal. CHECKOUT.ORDER.APPROVEDيُتجاهل عمدًا: التأكيد عنده يحتجز ليالي مقابل مال لم يتحرك.
مسار webhook
POST /api/webhooks/payments/:provider، مع stripe أو paypal مزودًا. لا يحتاج إلى تسجيل دخول: يُفحص كل طلب بتوقيع المزود نفسه على الجسم الخام وطابع زمني، فلا يمكن تعديله ولا إعادة تشغيله. التوقيع الخاطئ يُجاب بـ 401 والمزود المجهول بـ 404؛ وكل حدث مُتحقق منه يُجاب بـ 200، ومنها المتعلقة بشيء آخر، كي لا يعيد المزود محاولتها أيامًا.
يجب أن تكون واجهة API متاحة من الإنترنت على ذلك العنوان. خلف وكيل، مرّر جسم الطلب دون مساس: الجسم المعاد تسلسله لم يعد يطابق توقيعه.
الإلغاءات والاستردادات
- يلغي الضيف من صفحة حجزه، مجانًا، ما دام الحجز قيد الانتظار أو مؤكدًا وكان وقت الوصول المحلي بعد سبعة أيام على الأقل. وداخل تلك المهلة تطلب الصفحة التواصل مع المضيف. نص السياسة الذي يقرؤه الضيوف هو إعداد
cancellation_policy؛ والأيام السبعة ثابت في واجهة API. - يلغي المضيف من لوحة التحكم في أي وقت قبل اكتمال الحجز.
- المنتج لا يعيد المال أبدًا. إلغاء حجز مدفوع يعلّم دفعه
refund_pending؛ ويردّه المضيف في لوحة Stripe أو PayPal نفسها، وwebhook الاسترداد هو ما يكتبrefunded. والحجز المدفوع عند الوصول يردّه المضيف مباشرة. - الإلغاء يحرّر الليالي فورًا، ويخطر كل مسؤول يستطيع عرض الحجوزات.
من يستطيع فعل ماذا
| الصلاحية | يتيح |
|---|---|
bookings.view | قراءة الحجوزات، وتلقي إشعارات المدفوعات وطلبات الدفع عند الوصول والإلغاءات |
bookings.edit | تأكيد وتسجيل الوصول وإكمال وإلغاء، والملاحظة الداخلية |
settings.view, settings.edit | رؤية تبويب المدفوعات وتغييره: الدفع عند الوصول وسياسة الإلغاء |
الدوران المضافان يملكان bookings.view وbookings.edit؛ وحده Super Admin يملك صلاحيات الإعدادات.
المسارات خلفها
يجيب GET /api/payments/methods بـ { methods: [{ method, hosted }], allowPayOnArrival, simulated }. وPOST /api/bookings عام ويجيب بمرجع الحجز وaccess_token الخاص به وحالته وحالة الدفع والإجمالي، وredirect_url إلى صفحة المعالج لطريقة مستضافة؛ والطريقة التي لا تحتويها القائمة تُجاب بـ 422 مع payment_method_unavailable. ومسارات الضيف نفسه تأخذ ذلك الرمز، أو البريد الذي أُجري به الحجز: GET /api/bookings/:reference وPOST /api/bookings/:reference/settle (التقاط صفحة العودة) و/cancel و/review. وتقرأ لوحة التحكم الحجوزات وتحرّكها بـ GET وPATCH /api/bookings/manage/:reference، والأخير مع { "action": "confirm" } أو check_in أو complete أو cancel، وadmin_notes.
كيف تتكامل بوابات الدفع
مُضمَّن مع مشترياتك. سجّل الدخول لقراءته، أو افتحه في ملف التنزيل.
واجهة البوابة، وكيف تجد الطريقة معالجها، وكيف تضيف بوابتك.