---
title: "مطابقة الطلبات في الثانية صباحًا"
description: "يجمع Keforo App الطلبات من Shopify وWooCommerce وجداول Google في مكان واحد. لم يكن الجزء الصعب واجهات البرمجة يومًا. بل الاتفاق على ما هو الطلب حين تختلف ثلاثة أنظمة."
date: 2026-06-28
tags: ["keforo", "طوابير", "backend"]
language: ar
canonical: https://aissamirhir.com/ar/blog/reconciling-orders-at-2am
source: aissamirhir.com
---
بدأ Keforo App بوعد صغير: كل طلب يستلمه المتجر، من كل قناة يبيع عبرها، في قائمة واحدة، صحيحة. Shopify وWooCommerce وجدول Google الذي يستعمله الفريق حين يراسل عميلٌ على WhatsApp. ثلاثة مصادر، حقيقة واحدة.

أخذت واجهات البرمجة أسبوعًا. أما الاتفاق على ما هو الطلب فأخذ بقية السنة.

## ثلاثة مصادر، ثلاث ساعات

يرسل Shopify رابط ويب (webhook) لحظة تغيّر أي شيء ويتيح لك إعادة جلب أي شيء. ويرسل WooCommerce روابط ويب أيضًا، حين يشاء المكوّن الإضافي، وطوابعه الزمنية بتوقيت المتجر. أما الجدول فلا رابط ويب له، ولا معرّفات سوى رقم الصف، والبشر يعدّلونه مباشرة، وبأثر رجعي أحيانًا.

ومن هنا القاعدة الأولى: **لا تثق أبدًا بأن الحدث هو القصة كاملة**. كل رابط ويب تلميح يقول «أعِد جلب هذا الطلب». وكل تشغيل مجدوَل يعيد قراءة كل ما تغيّر منذ علامة مرجعية، لكل مصدر، بساعة ذاك المصدر.

## طلب قانوني واحد

يُطبَّع كل مصدر إلى الشكل نفسه قبل أن يلمسه أي شيء آخر.

```typescript title="order.ts"
interface CanonicalOrder {
  source: 'shopify' | 'woocommerce' | 'sheet'
  externalId: string // Shopify id, Woo id, or "sheet:<rowKey>"
  revision: string // updated_at, or a hash of the row
  fingerprint: string // sha256 of the normalised payload
  customer: { email: string | null; phone: string | null; name: string }
  lines: { sku: string; quantity: number; unitPrice: number }[]
  total: number
  currency: string
  status: 'open' | 'paid' | 'shipped' | 'cancelled'
  placedAt: string // ISO, UTC
  notes: string[]
}
```

`revision` هو ما يقول المصدر إنه تغيّر. و`fingerprint` هو ما تغيّر فعلًا. الفرق مهم: يرفع WooCommerce قيمة `updated_at` حين يفتح أحدهم الطلب في لوحة الإدارة دون أن يمسّه.

## استيعاب تكراريّ

الكتابات عمليات upsert مفتاحها `(source, externalId)`. إذا طابقت البصمة ما هو مخزّن، تُتجاوَز الكتابة. أي عامل يمكنه معالجة أي مهمة، بأي ترتيب، أكثر من مرة، وتنتهي قاعدة البيانات إلى الحالة نفسها.

```typescript
await Orders.updateOne(
  { source: order.source, externalId: order.externalId },
  {
    $setOnInsert: { firstSeenAt: new Date() },
    $set: { ...order, syncedAt: new Date() },
  },
  { upsert: true },
)
```

تجاوز البصمات غير المتغيّرة هو ما يجعل إعادة قراءة كل شيء رخيصة بما يكفي لفعلها كل بضع دقائق.

## حين تختلف المصادر

قد يوجد الطلب الواقعي نفسه في مصدرَين: طلب Shopify كتبه الفريق أيضًا في الجدول ليضيف ملاحظة تسليم. مطابقتهما تقديرية (البريد أو الهاتف نفسه، المجموع نفسه، ضمن نافذة زمنية)، والمطابقة ليست دمجًا.

«الكاتب الأخير يفوز» خاطئ حين يتعلّق الأمر بالمال. القواعد لكل حقل على حدة.

| الحقل | من يفوز | لماذا |
|---|---|---|
| `status`, `total`, `lines` | القناة التي استلمت الدفع | هي نظام السجلّ للبيع |
| `customer.phone` | أي مصدر لديه رقم | القنوات تفتقده غالبًا؛ والجدول يملكه |
| `notes` | تُدمَج وتُزال المكرّرات | البشر يضيفون السياق ولا أحد يحذفه |
| `placedAt` | الأقدم | الجدول يُكتب بعد وقوع الحدث |

كل ما لا تستطيع القواعد حسمه يصير **تعارضًا**: يُخزَّن، ويُعرَض، ويُترَك لإنسان. التشغيل النموذجي على متجر مزدحم يستغرق نحو ثانية ونصف ويرصد تعارضَين أو ثلاثة. لا يُحلّ أي منها آليًا. وبقي ذاك العدد منخفضًا تحديدًا لأن القواعد ترفض التخمين.

> [!NOTE]
> الجدول مصدر حقيقة أيضًا. من المغري معاملته نسخةً ناقصة، لكنه المكان الوحيد الذي يكتب فيه الفريق ما قاله العميل على الهاتف. تعديلات البشر مراجعات كغيرها، وتحصل على معاملة البصمة نفسها.

## الطوابير، والتراجع، وجزء «الثانية صباحًا»

لكل مصدر طابوره الخاص على Redis. المهمة طلب واحد، لا مصدر واحد، فرابط Woo المعطوب لا يعطّل Shopify. تستعمل إعادات المحاولة تراجعًا أسّيًا مع تشويش، وطابور رسائل ميتة بعد خمس محاولات؛ وتلك الرسائل الميتة هي ما أقرأه في الصباح.

عنوان هذه الملاحظة ليس مجازًا. كانت النسخة الأولى تطابق كل شيء في مهمة ليلية واحدة، وحين كانت تخفق في الثانية صباحًا كانت تخفق كلها معًا. مهام لكل طلب وكتابات تكرارية جعلت الإخفاقات صغيرة وقابلة للاستئناف ورتيبة. هذه هي المعمارية كلها: اجعل كل خطوة آمنة للتكرار، وكرّر بحرّية.

## ما الذي كنت سأفعله بشكل مختلف

البصمة من اليوم الأول؛ أضفتها بعد حادثة الإشعارات المكرّرة الثانية. تخزين الحمولة الخام بجانب الطلب القانوني، لأن كل سؤال من نوع «لماذا يقول هذا 43 بدلًا من 42» ينتهي في البيانات الخام. ومعاملة الجدول مصدرًا من الدرجة الأولى منذ البداية، بدلًا من أن يبقى فكرةً لاحقة لشهرَين.
