سوق للخدمات المستقلة تخدم فيه المنصة نفسها نيّتين متقابلتين: من يبيع مهارة ومن يشتريها. يتصفح الطرفان الإعلانات نفسها، ويتبادلان الرسائل في الخيوط نفسها، ويقرآن التقييمات نفسها — لكنهما لا يريدان رؤيتها بالطريقة نفسها تقريبًا أبدًا.
المشكلة والقيود
الصعوبة في سوق ذي وجهين ليست في عمليات الإنشاء والقراءة والتعديل والحذف، بل في أن المشتري والبائع ينتظران إجابات مختلفة من بيانات متطابقة، وأن المال جزء من المعادلة. ومن ذلك نشأ قيدان: الدور الذي تُنفَّذ به الطلبية لا يمكن أن يأتي من العميل أبدًا، ولا المبلغ المحصَّل كذلك. وجاء قيد ثالث من صفحات الإعلانات — فشبكة بطاقات تعرض تقييمات بالنجوم لا تحتمل تجميع مجموعة المراجعات مع كل عملية عرض.
النهج
- نموذج
Userواحد يحمل حقلًا منطقيًاisSellerمضمَّنًا في حمولة JWT عند تسجيل الدخول. عندها يرشّحgetOrdersحسب الدور المقروء من الرمز — نقطة نهاية واحدة، وإجابتان صحيحتان، دون أي وسيط دور يرسله العميل ويمكن تزويره. - يعيش رمز JWT في كعكة httpOnly لا في
localStorage، فلا تستطيع السكربتات في المتصفح قراءته. - إنشاء
PaymentIntentمن Stripe في الخادم، مع قراءة المبلغ من سجل الإعلان المحفوظ — فلا يستقبل المتصفح سوىclientSecretولا يصرّح بسعر أبدًا. - تقييمات غير مُطبَّعة على الإعلان في صورة زوج تراكمي
totalStars+starNumber، فتحسب البطاقة متوسطها من عددين صحيحين تملكهما أصلًا. - نمذجة المحادثات والرسائل كمجموعتين منفصلتين، ما يُبقي تحميل قائمة الخيوط مستقلًا عن حجم الرسائل داخلها.
الوصف الكامل:
- client (React) — httpOnly JWT cookie → Express API — role read from JWT — amount from listing
- Express API — role read from JWT — amount from listing → MongoDB — listings · orders · — reviews · messages
- Express API — role read from JWT — amount from listing → Stripe — PaymentIntent
المفاضلات
Decision
تقييم غير مُطبَّع على الإعلان بدل تجميع المراجعات عند القراءة: عددان صحيحان على المستند الذي تجلبه البطاقة أصلًا، بدل ربط لكل بطاقة في الشبكة — مع تقبّل وجوب تحديث الزوج ضمن معاملة واحدة مع كل مراجعة جديدة، وأن حذف مراجعة يستلزم تصحيحًا صريحًا لا مجرد اختفاء من إعادة العدّ.
Decision
الدور محمول في الرمز بدل إعادة قراءته من قاعدة البيانات مع كل طلب: يحذف عملية قراءة من كل استدعاء موثّق، مقابل تجمّد الدور طوال عمر الرمز — فالبائع الذي يغيّر وضعه يبقى على الواجهة القديمة حتى يُعاد إصدار الرمز.
المفاضلة الأولى من هاتين تحمل ثمنها داخل جملة تابعة: التقييم المحذوف يحتاج تصحيحًا صريحًا. ها هي الثنائية مباشرةً — أضِف تقييمات فتبقى صحيحة، واحذف واحدًا ولاحظ كيف تنحرف:
الثنائية متطابقة مع المجموعة — كل إضافة حدّثت الاثنين معًا.
المستندات المقروءة لعرض هذه الشبكة: 24 مع الثنائية المخزَّنة · 312 بإعادة الحساب لكل بطاقة
عرض توضيحي لنموذج البيانات، يُحسب مباشرةً داخل متصفحك — نفس التحديث ذي العددين الذي تنفّذه المنصة. ليست بطاقات حقيقية ولا تقييمات حقيقية ولا أزمنة استعلام مقيسة.
النتائج
دورة سوق عملية: نشر إعلان، والتحاور بشأنه، والدفع عبر Stripe، وترك مراجعة — مع اتخاذ قرارات التفويض والتسعير في الخادم بدل ائتمان العميل عليها.
ما كنت سأفعله بشكل مختلف
نقل إنشاء الطلب إلى خطّاف ويب (webhook) من Stripe. فكما هو الآن، يُكتب
سطر الطلب عند إنشاء PaymentIntent، أي قبل تأكيد الدفع، ما يعني أن سلة
متروكة تخلّف طلبًا محفوظًا لا يخفيه سوى مرشّح isCompleted — البيانات
صادقة لكن المجموعة ليست نظيفة، والمرجع الحاسم يجب أن يكون نداء مزوّد
الدفع نفسه. وكنت سأنقل أيضًا العملة usd المكتوبة في الشيفرة إلى
الإعدادات.