ربط Laravel بـ Xero دون أن تفقد المستأجر
يستبدل Xero رمز التحديث في كل تجديد، فيتحول عاملان متزامنان إلى مؤسسة مفصولة إلى الأبد. هذا الخلل، والأربعة التي تليه.
يُطلَق التكامل يوم ثلاثاء. يدفع الفواتير، ويسحب المدفوعات، ويتوقف فريق المالية عن نسخ الأرقام بين شاشتين، ويرضى الجميع. وبعد أحد عشر يومًا تظهر مؤسسة أحد العملاء مفصولة ولا تعود إلا بمروره من جديد على شاشة الموافقة. لم يُنشر شيء. ولم يلمس أحد الشيفرة.
ما حدث أن اثنين من عمّال الطابور جدّدا الرمز نفسه في الثانية نفسها.
رمز التحديث يُستعمل مرة واحدة
رمز الوصول في Xero يعيش ثلاثين دقيقة. هذا الجزء يتدبّره الجميع. ما يوقع الفرق هو أن التجديد يمنحك رمز تحديث جديدًا ويُبطل فورًا الذي أرسلته. هناك مهلة تسامح ستون ثانية على القديم، وبعدها يزول.
تخيّل مهمتين للمستأجر نفسه تبدآن بفارق أجزاء من الثانية، تجدان رمز وصول منتهيًا، وتناديان نقطة التجديد. تنال الأولى الرمزين B وتخزّنهما. والثانية أرسلت الرمز القديم نفسه، وداخل مهلة التسامح تنال هي أيضًا ردًا صالحًا - زوجًا آخر، الرمزين C
- فتكتبهما فوق الأول. صار الصف يحمل C، وآخر إصدار من Xero كان C، وكل شيء يبدو سليمًا. أعد الأمر تحت حمل حقيقي بعد استهلاك مهلة التسامح، فيفشل النداء الثاني، ولا يكتب معالج الأخطاء شيئًا، ويبقى الصف حاملًا رمز تحديث محترقًا. المؤسسة مفصولة، وعلاجها الوحيد موافقة جديدة.
الدفاع هو أن يُسمَح لعملية واحدة بالضبط بتجديد مستأجر بعينه، وأن ينتظر الباقون نتيجتها بدل أن يجدّدوا لأنفسهم.
public function accessToken(XeroConnection $connection): string
{
if ($connection->expires_at->isAfter(now()->addMinutes(2))) {
return $connection->access_token;
}
return Cache::lock("xero:refresh:{$connection->tenant_id}", 30)
->block(20, function () use ($connection) {
$connection->refresh(); // أعد القراءة؛ قد يكون غيرك سبقك
if ($connection->expires_at->isAfter(now()->addMinutes(2))) {
return $connection->access_token;
}
return $this->exchange($connection);
});
}ثلاثة تفاصيل هناك تحمل البناء. هامش الدقيقتين يمنع تسليم رمز إلى مهمة ستقضي
تسعين ثانية في الطابور خلف طلب بطيء. وإعادة القراءة داخل القفل تجعل العامل
الخاسر رخيصًا - يستيقظ فيجد رمزًا طازجًا فيستعمله. واستعمال block بدل get
يعني أن العامل الثاني ينتظر بدل أن يعيد false ويُفشل مهمة لا عيب فيها.
اكتب الزوج الجديد داخل معاملة، وعامِل التبادل الفاشل بوصفه تغيّر حالة لا استثناء. إن قال Xero إن رمز التحديث غير صالح فالاتصال ميت؛ وتعليمه ميتًا وإخبار العميل هو السلوك الصحيح، وإعادة المحاولة أربعين مرة ليست كذلك.
الاتصال ليس شركة
الإذن ليس لمستخدم ولا لشركة عميلك. إنه لمستأجر يُعرَّف بـ tenantId يعود من
نقطة الاتصالات بعد الموافقة، وشخص واحد يضغط على شاشة الموافقة قد يمنحك ثلاثة
منها إن كان يدير ثلاث مؤسسات.
يهمّ هذا من اليوم الأول لأنه يحدّد مخططك. كل سجل تزامنه يحمل المستأجر الذي يخصّه، وكل نداء يرسل ترويسة ذلك المستأجر، وكل استعلام يبحث عن "فاتورة Xero لهذا الطلب" يرشّح عليه. والفرق التي تتخطى ذلك لأن أول عميل كانت لديه مؤسسة واحدة تكتب الترحيل لاحقًا أثناء عُطل، وهو أسوأ وقت لإضافة عمود إلى جدول فيه مليونا صف.
ويهمّ أيضًا لأن المجموعة ليست ثابتة. يمكن إزالة مؤسسة من تطبيقك من داخل واجهة Xero نفسها، بيد شخص لم يرَ واجهتك قط. أعد فحص نقطة الاتصالات دوريًا لا عند الموافقة فقط، وطابقها مع جدولك.
الحد أربعة حدود
لك ستون نداءً في الدقيقة لكل مستأجر، وخمسة آلاف في اليوم لكل مستأجر، وعشرة آلاف
في الدقيقة لتطبيقك كله، وسقف للطلبات المتزامنة. تفشل جميعها بالطريقة نفسها، بـ
429 وترويسة Retry-After، لكنها تعني أشياء مختلفة، والرد يخبرك أيها بلغت في
ترويسة X-Rate-Limit-Problem.
حدّ الدقيقة مشكلة إيقاع، وعلاجه إبطاء طابور ذلك المستأجر. وحدّ اليوم مشكلة تصميم، ولا تراجع أسّي يصلحه - أنت تجلب ما لديك أصلًا. أما حدّ التطبيق فهو الذي يُفسد الثلاثاء على كل عملائك دفعة واحدة لأن استيرادًا أوّليًا لعميل واحد يعمل، وهذه هي الحجة لطابور لكل مستأجر بدل طابور مشترك.
احترم Retry-After حرفيًا. وسيط RateLimited في Laravel على المهمة، مُعادًا
بعدد الثواني نفسه الذي أعطته الترويسة، هو التنفيذ كله، وهو أفضل من أي تراجع أسّي
تكتبه، لأن الرقم ليس تخمينًا.
الـ webhooks تقول إن شيئًا تغيّر، لا ما الذي تغيّر
حمولة webhook في Xero تحمل نوع المورد ومستأجرًا ومعرّفًا وختمًا زمنيًا. ولا تحمل الفاتورة. تصلك الأخبار ثم تذهب أنت فتجلبها، ما يجعل الـ webhook تلميحًا بالقراءة لا كتابة.
أمران في النقطة غريبان بما يكفي لتعرفهما قبل بنائها. يتحقق Xero منها برسالة "نيّة الاستقبال" التي عليك الرد عليها صحيحًا قبل أن يعمل الاشتراك، وفحص التوقيع هو HMAC على الجسم الخام، فأي شيء يعيد تسلسل الطلب قبل التجزئة سيفشل بصورة تشبه تمامًا مفتاحًا خاطئًا. والتسليم قليل الصبر: أكّد بـ 200 ولا شيء غيرها، وضع المعرّف في طابور، ثم اجلب بعد ذلك.
وابنِ مسار الاستعلام الدوري على أي حال. استعلام مرشَّح بـ If-Modified-Since
يعطيك كل ما تغيّر منذ آخر مزامنة ناجحة، ولا يعنيه أكانت نقطتك تعمل أم لا. هذا
الاستعلام وحده هو ما يجعل العطل تأخيرًا لا ثقبًا في بياناتك.
المطابقة هي المشروع
الهندسة أعلاه أسبوعان. وما يستغرق الباقي هو تحديد ما هو صحيح.
في تطبيقك فاتورة. وفي Xero فاتورة. وفريق المالية عدّل التي في Xero يوم الخميس لأنه يعمل هناك. وأحدهم أصدر إشعار دائن عليها. ووصل دفع بمبلغ لا يتوقعه أيٌّ من السجلين لأن العميل سدّد ثلاث فواتير بحوالة واحدة. لا شيء من ذلك سؤال عن API.
القرارات التي يجب كتابتها قبل أن تنفع الشيفرة: أي نظام يملك أي حقل، وماذا يحدث حين يغيّر الطرفان الحقل نفسه، وهل إبطال فاتورة في Xero يُبطل الطلب في تطبيقك أم يعلّمه فقط، وكيف يُوزَّع دفع جزئي، وماذا تفعل ترقيم فواتيرك حين يكون Xero هو مرجع الترقيم في بعض الولايات القضائية دون غيرها. عملنا في هندسة التكامل هو هذا الحديث في معظمه، ونداءات الـ API هي ما يتساقط منه.
واجعل كل كتابة غير متأثرة بالتكرار ما دمت هنا. يقبل Xero مفتاح تفرّد للعملية، والطابور تحتك يسلّم مرة واحدة على الأقل، فالمهمة المكررة ستصير وإلا فاتورة مكررة في دفاتر شخص آخر.
متى لا تبني هذا
إن كان المطلوب أن يرى فريق المالية الإيراد حسب المنتج، فتصدير وجدول بيانات يوصلانه هذا الأسبوع، والمزامنة في اتجاهين توصله بعد شهرين. وإن كان الحجم أربعين فاتورة في الشهر فالحدّ اليومي ليس مشكلتك ولا شيء مما سبق كذلك - دفعة ليلية تكفي وتكلّف عُشر الشيفرة.
ابنِ المزامنة حين يجعل عدد السجلات الإدخال اليدوي كلفة حقيقية، وحين يُحرَّر الطرفان فعلًا، وحين يحتاج شيء لاحق أن يتفاعل مع دفعة خلال دقائق. في هذه الحالات تدفع أعمال المطابقة ثمن نفسها. وخارجها هي هندسة كثيرة لإلغاء مهمة كانت تكلّف أحدهم عشرين دقيقة أسبوعيًا.
وإن كان دفتر الأستاذ في Sage لا في Xero، فمعظم ما سبق يبقى صالحًا ومسألة الوصول لا - وتلك مقالة أخرى، لأن Sage ليس منتجًا واحدًا.
