هندسة
أرسلتَ طلب الخصم ومات الاتصال. هل تم الخصم؟
By Clasify Team · · 5 min read
كل تكامل مدفوعات يلتقي في النهاية بالسؤال نفسه، وهو ليس سؤالًا عن المال. أرسلتَ طلب خصم. مات الاتصال قبل أن تعود الاستجابة. هل تم الخصم؟
لا سبيل لك إلى المعرفة من جهتك. إعادة المحاولة قد تخصم من العميل مرتين. وعدم إعادتها قد تترك اشتراكًا مدفوعًا بلا تجديد. وأيًا كان اختيارك، ستختاره ثانيةً عند المهلة التالية، ثم التي تليها.
هذا هو الجزء الصعب فعلًا في المدفوعات. نموذج البطاقة ليس صعبًا. الصعب هو مصالحة نظام موزّع مع دفتر حسابات يملكه طرف آخر، عبر الأعطال وإعادات المحاولة والـ webhooks المكررة.
قرّر قبل الخصم لا بعده
الانعكاس الأول أن تسجّل الدفعة حين تنجح. وهذا معكوس تمامًا، لأن اللحظة التي تحتاج فيها السجل هي اللحظة التي لا تعرف فيها هل نجحت.
لذلك يأتي السجل أولًا. قبل أي محاولة تجديد، ننشئ صفَّ مرساة مفتاحه idempotency key حتمي — حتمي بمعنى أنه مشتق من الاشتراك ومن فترة الفوترة، فالتجديد نفسه يُنتج المفتاح نفسه دائمًا مهما كانت العملية التي حسبته أو وقت حسابه.
وهذا المفتاح يحمل قيد تفرّد، والتفرّد هو ما يقوم بالعمل الحقيقي. فإنشاء المرساة مطالبة ذرّية. وإن كان تمريرٌ سابق قد أنشأها — لأنه انهار في منتصفه، أو أطلقت مهمة مكررة، أو وصلت إعادة محاولة — تعارَض الإدخال، فنجلب الصف الموجود بدل إنشاء ثانٍ.
والنتيجة المهمة هي ما يحدث بعد ذلك. فبعد العثور على مرساة قائمة، لا يعيد الكود الخصم بشكل أعمى، بل ينظر في الحالة التي بلغتها المرساة ويقرر تبعًا لها: يخصم، أو يصالح مقابل مزوّد الخدمة، أو يعيد تطبيق نتيجة سبق الحصول عليها. التمرير المنهار يترك أثرًا، والأثر هو ما يقرؤه التمرير التالي.
اللحظة التي تحتاج فيها أشدّ الحاجة إلى سجل للخصم هي اللحظة التي لا تستطيع فيها معرفة هل حدث الخصم.
التزامن يُحسم بكتابات شرطية لا بأقفال
بعد وجود الصف، تتخذ السباقات المتبقية شكلًا واحدًا: عمليتان تنظران إلى المرساة نفسها وتقرران التصرف معًا.
القراءة ثم الكتابة تخسر هذا السباق. كلتاهما تقرأ `failed`، وكلتاهما تقرر إعادة المحاولة، وكلتاهما تخصم. والعلاج ليس mutex، بل جعل القرار والكتابة عبارةً شرطية واحدة، وترك قاعدة البيانات تقول من الفائز.
إعادة محاولة دفعة فاشلة تحديث شرطي لا يطابق إلا صفًا في حالة الفشل حاليًا ويقلبه إلى الانتظار. تشغّله محاولتان؛ واحدة فقط تُحدِّث صفًا؛ والتي طابقت هي المسموح لها بالخصم. وتسوية دفعة غير مدفوعة تعمل بالطريقة نفسها، وكذلك ردّ دفعة مدفوعة. وعدد الصفوف التي طابقتها العبارة **هو** الجواب عن سؤال "هل أنا الفائز؟"
وأدقّ الحالات استعادة مرساة عالقة في الانتظار. تريد لتمرير الاستعادة أن ينقذ خصمًا مات فعلًا، ولا تريده مطلقًا أن يسرق خصمًا لا يزال جاريًا. لذلك نطابق على طابع زمني: مرساة في الانتظار فقط، ولا تزال تحمل مرجعها المؤقت، ولم تُمسّ لمدة تتجاوز حدّ التقادم. فمرساة التمرير الحي حُدِّثت قبل لحظات، فتسقط من الشرط ولا تُسرق أثناء الخصم. ولأن كتابة الاستعادة نفسها ترفع الطابع الزمني، فإن تمرير استعادة ثانيًا يجري في اللحظة ذاتها يطابق صفرًا من الصفوف.
لا شيء هنا قفل موزّع. كله قيود تفرّد وتحديثات شرطية، وهي بالضبط ما تجيده قاعدة بياناتك أصلًا.
الـ webhooks تصل أكثر من مرة، بالتصميم
المزوّدون يعيدون إرسال الـ webhooks. وهذا ليس خللًا في أنظمتهم، بل هو ضمانهم للتسليم. ومعناه أن معالجك سيرى الحدث نفسه مرتين، وعليه ألا يتصرف مرتين.
نحن نسجّل كل webhook وارد مقابل معرّف معاملة المزوّد، مع قيد تفرّد عليه. والإدخال نفسه هو آلية منع التكرار: المكرر يتعارض فيُجاب بوصفه نسخة مكررة بدل إعادة معالجته. وقفل الإعادة هو الآلية نفسها المستخدمة في مرساة التجديد، مطبَّقة على مشكلة أخرى.
وأمران آخران يستحقان التنفيذ عند هذا الحد. تحقّق من التوقيع قبل أن تثق بأي حقل في الجسم، واحفظ الحكم — نحن نحتفظ بصلاحية الـ HMAC وبحكم عام بالنجاح أو الفشل إلى جانب الحدث، فيصبح النداء غير الموقّع أو المُلاعَب صفًا يمكنك النظر فيه لاحقًا بدل شيء تبخّر.
واحفظ ملخّصًا لا الحمولة. نحتفظ بمجموعة مضغوطة من الحقول الآمنة للتدقيق، وبلا أي بيانات بطاقة عن قصد. فسجل الـ webhooks جدول ينسى الناس التفكير فيه، وأرخص طريقة لئلا تُسرَّب منه بيانات بطاقة أبدًا هي ألا تضع فيه بيانات بطاقة أبدًا.
ما سنقوله لفريق آخر
- اكتب السجل قبل الأثر الجانبي، بمفتاح حتمي. إن لم تأخذ إلا فكرة واحدة، فخذ هذه.
- اجعل "هل فزت بهذا السباق؟" قيمةً تعيدها قاعدة البيانات، لا قرارًا يتخذه كودك بعد قراءة.
- عامل كل webhook بوصفه مكررًا حتى يقول قيد التفرّد غير ذلك.
- اجعل مهام الاستعادة تُثبت التقادم بطابع زمني، وإلا سرقت عمليةً حية في النهاية.
- افترض أن كل عملية قد تموت بين أي سطرين، واسأل: ماذا سترى العملية التالية؟ المرساة موجودة كي لا يكون الجواب "لا شيء".
ونحن صريحون في حدود الكلام: هذا التصميم هو ما تعمل عليه فوترتنا، وأنماط الفشل أعلاه هي ما بُني ليصمد أمامها. وصحّة المدفوعات ليست شيئًا تركّبه بعد أول خصم مزدوج — فالمقصد كله أن الحادثة المكلفة هي التي لا تقع أبدًا.
ننفّذ هذا النوع من العمل لفرق أخرى ضمن هندسة المدفوعات والدفع الإلكتروني، وهو يجاور عملنا في تطوير منصات SaaS.
كل المقالات