دليل التكامل مع أنظمة ERP والأنظمة الأخرى
حدّد نطاق التكامل قبل بنائه: ما الذي يصل إليه الـ API، وما لا يصل إليه، وأين تقع الحدود
هذا الدليل موجَّه إلى المطوِّر الذي يحدّد نطاق تكامل بين FreeMaint ونظام آخر — نظام ERP، أو مؤرِّخ بيانات SCADA، أو حزمة مستودعات أو محاسبة. تشرح بقية مقالات هذا القسم كيفية استدعاء الواجهة؛ أما هذه المقالة فتتحدث عن الحدود عن قصد، لأن الخطأ المكلف ليس ترويسة خاطئة، بل أن تكتشف في منتصف البناء أن قدرةً افترضتَ وجودها ليست موجودة أصلاً. كل حدٍّ مذكور أدناه مذكور عن عمد.
مفتاح واحد، ثلاثة أبواب
مفتاح الـ API (يبدأ بـ fmk_) يُنشئه مسؤول من إعدادات الشركة > API (/company-settings/api)، أو ينشئه شخص لنفسه من الملف الشخصي > الأمان > مفاتيح API، ويُرسَل بالشكل Authorization: Bearer fmk_... . وهو يوثّق ثلاثة أشياء بالضبط: نقطة نهاية GraphQL عبر POST /api/v1/graphql، ونقطة استقبال بيانات الطاقة، و/public-api/whoami و/public-api/ping. وهو لا يوثّق مسارات REST تحت /api/v1/{resource} — فتلك هي واجهة التطبيق الخاصة به وتتطلب جلسة مستخدم (JWT). ابْنِ تكاملك على GraphQL: فهو السطح الذي يصل إليه المفتاح.
ما الذي يمنحك إياه مفتاح الـ API
يُعرض المفتاح الخام مرة واحدة فقط عند الإنشاء، ولا يمكن استرجاعه بعد ذلك أبداً: لا يُخزَّن سوى تجزئة SHA-256، إضافةً إلى الأحرف الستة التي تلي fmk_ حتى تتمكن القائمة من التعرّف على المفتاح على الشاشة.
الاسم
تسمية مقروءة للبشر — «جسر Oracle ERP»، «كشك الخط 2». أصدِر مفتاحاً لكل نظام مستهلِك حتى تتمكن من إبطال أحدها دون إزعاج البقية.
مستوى الوصول
READ أو READ_STOCK أو FULL، وهي تسلسل هرمي مرتّب يُضاف فوق صلاحيات المالك. يقتصر READ على الاستعلامات وهو القيمة الافتراضية. ويضيف READ_STOCK عملية التعديل adjustStock. أما FULL فيتيح كل عملية يسمح بها دور المالك، بما في ذلك الإنشاء والتحديث والحذف — ونقطة استقبال بيانات الطاقة إذا كان المالك مخوّلًا بإنشاء قراءات الطاقة.
انتهاء الصلاحية
حقل expiresAt اختياري. بعد تلك اللحظة يتوقف المفتاح عن التوثيق. اتركه فارغاً لتكامل خدمي دائم، واضبطه لمقاول أو لتجربة تجريبية.
الإبطال
الحقل isActive مفتاح تشغيل خاص بكل مفتاح على حدة، ويسري مفعوله خلال ثوانٍ. ويُحتفظ بالسجل لأغراض التدقيق، فيبقى المفتاح المُبطَل قابلاً للتتبع بدل أن يختفي.
آخر استخدام
يسجّل lastUsedAt آخر نداء موثَّق — وهذا يكفي لاكتشاف مفتاح لم يعد أحد يستخدمه.
المالك
الشخص الذي يعمل المفتاح بصفته. يحمل كل استدعاء صلاحياته ونطاق رؤيته للبيانات، ويُوقِّع اسمُه ما يكتبه المفتاح. بالنسبة إلى نظام ERP، أنشئ مستخدمًا مخصصًا — «تكامل ERP» — بدور يغطي بالضبط ما يحتاجه الجسر، وسلّمه المفتاح.
ما لا يمنحك إياه مفتاح الـ API
هذه حدود حقيقية في التنفيذ الحالي، وليست إغفالاً في هذه الصفحة. إن كانت مراجعتك الأمنية تتوقف على أيٍّ منها، فصمِّم للالتفاف عليها الآن لا بعد انتهاء البناء.
- لا قائمة وحدات لكل مفتاح. يقرأ المفتاح ويكتب ما يسمح به دور مالكه — ولتضييقه، سلّمه إلى مالك دوره أضيق. وفوق ذلك لا تنطبق إلا مستويات الوصول الثلاثة: لا يمكنك أن تحدد على المفتاح نفسه الوحدات التي يجوز له استخدامها.
- لا قائمة سماح لعناوين IP. المفتاح صالح من أي مكان على الإنترنت. تعامل معه كسرّ: خزنة أو مدير الأسرار في منصّتك، ولا تضعه أبداً في مستودع شفرة أو قرص مشترك أو تذكرة دعم.
- لا سجل استخدام لكل مفتاح. لا يُحفظ سوى lastUsedAt. لا يوجد تاريخ نداءات لكل مفتاح، ولا حصة لكل مفتاح، ولا أثر يبيّن أي مفتاح نفّذ أي عملية. عدّاد الحصة اليومية محسوب على مستوى الشركة ومشترك بين جميع المفاتيح، فتكامل واحد منفلت قادر على استنفاد حصة البقية جميعاً.
- لا موافقة نيابةً عن أحد. لا يعتمد المفتاح أبدًا خطوة في سير العمل، حتى لو كان مالكه يستطيع ذلك: تبقى الموافقات فعلًا بشريًا داخل التطبيق.
- شركة واحدة لكل مفتاح. المفتاح مرتبط بالشركة التي أُنشئ فيها ولا يمكنه الوصول إلى بيانات شركة أخرى: فسجل شركة أخرى لا يُعثر عليه ببساطة.
ما يمكنك قراءته
يتيح GraphQL ما مجموعه 16 استعلامًا للقراءة: whoami وworkOrders وworkOrder(id) وassets وasset(id) وassetDowntimes وmeters(assetId) وparts وpart (بالمعرّف id أو الباركود) وstockMovements وlocations وvendors وcustomers وpurchaseOrders وpurchaseOrder(id | reference | erpReference) وfloorPlans(locationId). ويمكن تصفّح كل قائمة حتى نهايتها: فكلٌّ من workOrders وassets وassetDowntimes وmeters وparts وpurchaseOrders وlocations وvendors وcustomers يقبل limit (من 1 إلى 200) مع offset، كما تقبل workOrders وassets وparts وpurchaseOrders وlocations وvendors وcustomers أيضًا updatedSince للمزامنة التزايدية. والصفحة الافتراضية 50 صفًّا، باستثناء locations وvendors وcustomers التي تحتفظ بقيمتها الافتراضية السابقة وهي 200. وكل قائمة مرتّبة مع معيار ترتيب احتياطي على id لفضّ التساوي، فلا تتخطّى حدود الصفحة أي صف ولا تكرّره أبدًا. أما stockMovements فهو سجلّ المخزون الذي تُجري منه المطابقة: يُقرأ من الأقدم إلى الأحدث ويُتصفَّح عبر afterId — مرّر id آخر حركة تلقّيتها — وهي طريقة تبقى دقيقة حتى أثناء كتابة حركات جديدة، ويتيح لك since بدء استكمال البيانات السابقة من تاريخ معيّن. ولا تُعاد أبدًا أوامر العمل الموجودة في المهملات. ويُرشَّح purchaseOrders أيضًا حسب status وvendorId؛ وتأتي المبالغ بعملة الشركة، التي يحملها كل أمر في الحقل currency.
تحقّق من المفتاح واقرأ أوامر العمل في نداء واحد: curl -X POST https://freemaint.com/api/v1/graphql -H "Authorization: Bearer $FMK" -H "Content-Type: application/json" -d '{"query":"{ whoami { companyId tier } workOrders(limit: 5, status: \"OPEN\") { id reference title status priority dueDate } }"}'
ما يمكنك كتابته
اثنتا عشرة طفرة: الإنشاء والتحديث والحذف على أوامر العمل والأصول وقطع الغيار، إضافةً إلى adjustStock لحركة مخزون تحمل إشارة. وتمر جميعها عبر الخدمات نفسها التي يستخدمها التطبيق، فتتصرف المراجع (WO-… وASS-… وPART-…) وسطور التدقيق والإشعارات تماماً كما تتصرف في الواجهة. وللمخزون، فضّل adjustStock: فهي تكتب سطر تدقيق StockMovement في المعاملة نفسها وترفض إنزال الكمية دون الصفر، بينما updatePart(quantity) تعيّن قيمة مطلقة وتسجّل الفرق على أنه تعديل. وعلى قطعة يُتابَع مخزونها حسب الموقع، مرّر locationId إلى adjustStock؛ أما updatePart(quantity) فتُرفض هناك. أما الحادية عشرة، addMeterReading، فتسجّل قراءة عدّاد عبر الخدمة نفسها التي تستخدمها شاشة العدّادات، فتتفاعل مشغّلات العدّادات والصيانة الوقائية المعتمدة على العدّادات تماماً كما مع قراءة مُدخلة في التطبيق. أما الثانية عشرة، setPurchaseOrderErpReference، فتحفظ رقم مستند نظام ERP لديك على أمر الشراء، لتتمكن لاحقًا من مطابقة السجلين عبر purchaseOrder(erpReference: …). وتتطلب مفتاح وصول FULL ولا تُطلق عمدًا أي webhook: فلا يتلقى نظام ERP كتابته هو على شكل حدث أبدًا.
ما لا يغطيه الـ API إطلاقاً
يحتوي مخطّط GraphQL على أوامر العمل والأصول وتوقّفات الآلات وقطع الغيار وحركات مخزونها والعدّادات وأوامر الشراء والمواقع والموردين والعملاء ومخطّطات الطوابق. لا شيء غير ذلك. وما يلي أجزاء تعمل بكامل طاقتها في FreeMaint لكن لا تملك اليوم أي واجهة API: لا يمكن قراءتها ولا الكتابة إليها ولا الاشتراك فيها من تكامل خارجي، ولا يبلغها أي مزيج من الاستعلامات القائمة.
- عمليات الفحص — لا القوالب ولا الجولات المجدولة ولا النتائج المسجَّلة ولا رموز الإجراءات. لا يوجد استعلام فحص، ولا طفرة فحص، ولا حدث webhook للفحص.
- المستخدمون والأدوار والفرق — لا يمكنك سرد مستخدم ولا إنشاؤه ولا تعطيله عبر هذا الـ API. يجري تزويد المستخدمين داخل التطبيق؛ ويمكن أتمتته في باقة Enterprise عبر SCIM، وهي آلية منفصلة وليست جزءاً من هذا الـ API.
- المشاريع — لا استعلام ولا طفرة.
- طلبات الشراء — لا استعلام ولا طفرة. أما أوامر الشراء فيمكن قراءتها (purchaseOrders وpurchaseOrder)، ويمكن لنظام ERP أن يكتب عليها مرجعه (setPurchaseOrderErpReference)، وتُعلن عنها webhooks purchase_order.*، لكن لا يمكن إنشاء أمر أو تعديله أو استلامه عبر الـ API: فذلك يبقى داخل التطبيق.
- الملفات والصور والمرفقات — لا يمكنك رفع مستند أو صورة، ولا قراءتها مجدداً. صور أوامر العمل وصور قطع الغيار وأدلة عمليات الفحص كلها خارج الـ API. وأي عنوان يظهر في استجابة، مثل imageUrl الخاص بقطعة غيار، ليس مساراً للرفع.
- جداول الصيانة الوقائية وسجلات الوقت والمهام وقوائم التحقق — غير مكشوفة.
مهم
Webhooks — فهرس الأحداث
الـ Webhooks هي نصف «الدفع» من التكامل: بدل الاستقصاء الدوري، يرسل FreeMaint طلب POST إلى عنوانك في اللحظة التي يقع فيها الحدث. تُضبَط من إعدادات الشركة > Webhooks (/company-settings/webhooks)، وتشترك كل webhook بالأحداث التي تحتاجها. والفهرس قائمة ثابتة.
- work_order.created — تم إنشاء أمر عمل
- work_order.completed — بلغ أمر عمل حالة «مكتمل»
- work_order.status_changed — انتقل أمر عمل إلى أي حالة؛ وتحمل الحمولة new_status. يُطلَق إلى جانب work_order.completed الذي لم يتغيّر
- request.created — تم إرسال طلب صيانة
- request.status_changed — تمت الموافقة على طلب أو رفضه أو إلغاؤه
- part.low_stock — انخفضت قطعة غيار إلى حدّها الأدنى أو دونه
- part.out_of_stock — بلغت قطعة غيار الصفر؛ وهو حدث مستقل وليس صيغة من low_stock
- part.low_stock_summary — ملخّص يومي واحد لكل شركة يسرد القطع عند نقطة إعادة الطلب أو دونها، بما فيها القطع التي لم تتحرك قط
- part.out_of_stock_summary — الملخّص اليومي نفسه، للقطع التي بلغت الصفر
- part.consumed — استُهلكت قطعة غيار على أمر عمل أو أُرجعت منه؛ والحقل quantity يحمل إشارة
- compliance.chain_divergence — انكسرت سلسلة تجزئة التدقيق (Enterprise)
- asset.downtime_started — فُتِح توقّف آلة
- asset.downtime_ended — أُغلِق توقّف آلة؛ ويمكن الآن احتساب مدّته
- report.daily_summary — أعمال اليوم المنجزة، مرة يوميًا في الساعة التي تختارها
- purchase_order.created — أُنشئ أمر شراء؛ وتحمل الحمولة الأمر (reference وstatus وtotal_cost وcurrency وexpected_date وerp_reference) ومورّده
- purchase_order.status_changed — انتقل أمر شراء إلى حالة أخرى (معتمد، مطلوب، ملغى…)؛ وتحمل الحمولة previous_status وnew_status
- purchase_order.received — استُلم تسليم لأمر شراء (يُرسَل مع كل تسليم، بما في ذلك الجزئية)؛ وتسرد الحمولة كل بند مع quantity_ordered وquantity_received (هذا التسليم) وquantity_received_total وquantity_remaining، إضافة إلى receipt_id وfully_received وclosed_short؛ وتكون new_status هي PARTIALLY_RECEIVED ما دامت البضائع متوقعة، وRECEIVED عند اكتمال الأمر أو إغلاقه
تحذير
غلاف التسليم
كل تسليم من نوع GENERIC هو طلب POST جسمه هو الغلاف نفسه ذو المفاتيح الثلاثة: event وtimestamp وdata، حيث يُفهرس data بحسب الكيان. وترافقه ثلاث ترويسات. تأخذ X-FreeMaint-Signature الشكل t=<ثوانٍ unix>,v1=<hex>، حيث v1 هو HMAC-SHA256 للسلسلة الدقيقة «<t>.<الجسم الخام>» بمفتاح هو سرّ ذلك الـ webhook؛ وتكرّر X-FreeMaint-Event اسم الحدث؛ وتحدّد X-FreeMaint-Delivery-Id عملية التسليم. تحقّق من التوقيع مقابل الجسم الخام، قبل أي تحليل — فإعادة تسلسل JSON تغيّر البايتات ولن يتطابق التوقيع.
{ "event": "work_order.created", "timestamp": "2026-09-05T08:14:02.371Z", "data": { "work_order": { "id": 5521, "title": "تسرب في المضخة رقم 3", "status": "OPEN", "priority": "HIGH", "dueDate": "2026-09-08T00:00:00.000Z" } } }
الموثوقية والمراقبة
تُجرَّب عملية التسليم 3 مرات بتراجع أُسّي على أساس ثانيتين — نحو 2 ثانية ثم 4 ثم 8 — وتنتهي مهلة كل محاولة بعد 10 ثوانٍ. ويُعدّ الرد 2xx تسليماً ناجحاً. أما ردود 5xx وأخطاء الشبكة فتُعاد المحاولة عليها. ويُعامَل الرد 4xx كرفض نهائي ولا تُعاد المحاولة عليه إطلاقاً: فإن ردّت نقطة النهاية لديك بـ 400 أو 401 بسبب خلل في جانبك، فإن ذلك التسليم يتوقف عند هذا الحد ولن يستردّه إلا إعادة إرسال يدوية.
- كل تسليم محفوظ: الحدث، والحمولة المُرسَلة بالضبط، والحالة، ورمز HTTP الذي أعدتَه، وأول 500 حرف تقريباً من جسم استجابتك، وعدد المحاولات، ورسالة الخطأ.
- الحالات هي PENDING وDELIVERED وFAILED وRATE_LIMITED (بلوغ سقف Starter البالغ 100 تسليم في اليوم) وSKIPPED_TIER (لم تعد الباقة تمنح الحق في تلك الوجهة).
- يمكن إعادة إرسال أي تسليم محفوظ من سجل الـ webhook. فتُرسل الحمولة المحفوظة نفسها دون تغيير، وتبقى مفاتيح الأعمال داخلها كما هي، فلا يُرحِّل المستقبِلُ الذي يزيل التكرار بشكل صحيح العملية مرتين.
- يحمل كل webhook الحقول lastTriggeredAt وlastSuccessAt وlastFailureAt وfailureCount، وهذا يكفي لفحص السلامة دون فتح السجل الكامل.
- قائمة الـ webhooks وسجل تسليماتها قابلان للقراءة في كل الباقات، فيبقى التكامل الذي أوقفه تخفيض الباقة مرئياً بدل أن يموت في صمت.
- لا شيء ينبّهك عندما تبدأ التسليمات بالفشل — لا بريد ولا إشعار. فإن كان العطل الصامت مهماً لديك، فاستقصِ سجل التسليمات من نظام المراقبة الخاص بك.
الأمان
تُتحقَّق العناوين عند حفظ الـ webhook، والقواعد صارمة عن قصد: فالـ webhook طلب صادر تُصدره خوادمنا، ولذا فإن قاعدة متساهلة تصبح ناقلاً لهجمات SSRF.
- تُخزَّن المفاتيح على شكل تجزئات SHA-256 — ولا يمكن استرجاع القيمة الخام من FreeMaint بعد الإنشاء.
- يجب أن تستخدم عناوين الـ webhook بروتوكول https. ويُرفض http العادي وكل مخطط آخر.
- لا يُقبل سوى المنفذين 443 و8443.
- تُرفض مضيفات الاسترجاع المحلي والنطاق المحلي للوصلة والنطاقات الخاصة (127.x و10.x و192.168.x و172.16–31.x و::1 وما شابهها): يجب أن يكون المستقبِل متاحاً من الإنترنت العام. والنظام الموجود داخل مصنعك فقط يحتاج إلى وسيط عام أو نفق.
- تُرفض بيانات الاعتماد داخل العنوان (https://user:pass@host).
- لا تُتبَع عمليات إعادة التوجيه — فنقطة النهاية التي تسجّلها هي التي تتلقى الـ POST.
- لكل webhook سرّ توقيع خاص به، وهناك إجراء لتدوير السرّ. ويسري التدوير فوراً، فانشر السرّ الجديد في جانبك ضمن النافذة الزمنية نفسها.
- تتلقى وجهتا SLACK وTEAMS رسالة منسّقة بحسب المنصة وبدون أي ترويسة HMAC من FreeMaint: فبالنسبة إليهما تكون سرّية العنوان هي وسيلة التوثيق الوحيدة. استخدم GENERIC لكل ما تحتاج إلى التحقق منه تشفيرياً.
أنماط تعمل جيداً اليوم
هذه هي التكاملات التي يدعمها السطح الحالي بنظافة، مع التفصيل الذي يجعل تشغيل كل منها آمناً في الإنتاج.
عكس الاستهلاك في نظام ERP
اشترك في part.consumed. الحقل quantity يحمل إشارة — سالب عند الصرف للعمل، وموجب عند الإرجاع — وتحمل الحمولة الحقل stock_movement_id الذي يبقى ثابتاً عبر إعادات المحاولة وإعادات الإرسال، فاستخدمه كمفتاح للتكرار الآمن. وطفرة adjustStock لا تُطلق هذا الحدث عمداً، وهذا ما يمنع كتاباتك أنت من العودة إليك صدىً في حلقة مفرغة.
دفع تصحيحات المخزون إلى الداخل
استدعِ adjustStock بفارق موقَّع من جهتك. يستطيع مفتاح READ_STOCK القيام بذلك دون أن يتمكن من إنشاء أي شيء أو تعديله أو حذفه، ما يجعله خيارًا محكمًا لجسر مخزون أحادي الاتجاه.
إنشاء أوامر عمل من نظام آخر
استخدم createWorkOrder انطلاقاً من إنذار SCADA أو تذكرة مكتب مساعدة أو إشعار عطل من نظام ERP. ويحصل أمر العمل على مرجعه المعتاد ويُطلق work_order.created كأي أمر آخر.
إبقاء فهرس قطع الغيار متزامناً
يعيد parts(updatedSince:) ما تغيّر فقط منذ لحظة تختارها أنت. ويقبل createPart وupdatePart الحقل customReference — رمزك الخاص للقطعة — ثم يطابق عليه part(barcode:)، فتستطيع البحث عن قطعة بالمعرّف الذي يستخدمه نظام ERP لديك أصلاً. والتفرّد غير مفروض؛ ضمانه مسؤوليتك أنت.
إغلاق الحلقة عند الاكتمال
يحمل work_order.completed أمر العمل وحالته السابقة، وهذا يكفي لإغلاق إشعار صيانة في الطرف الآخر.
ما تحتاج إلى الاشتراك فيه
بوابتان منفصلتان: واحدة للـ API وأخرى للـ webhooks. ويمكن لشركة على باقة Starter تلقّي الـ webhooks دون امتلاك الـ API.
واجهة GraphQL ومفاتيح الـ API
من باقة Business فما فوق. ولا تستطيع باقتا Core وStarter إنشاء مفتاح إطلاقاً.
الحصة اليومية
5000 نداء في اليوم على Business، و100000 على Enterprise. وتُحسب لكل شركة ولكل عملية، وتُصفَّر عند 00:00 UTC — وتجميع العمليات في طلب HTTP واحد يوفّر رحلات الشبكة، لا الحصة.
Webhooks
من باقة Starter فما فوق، بسقف 100 تسليم في اليوم؛ وبلا حدود ابتداءً من Business.
وجهتا Slack وTeams
باقة Enterprise. أما الـ webhook العام فمتاح ابتداءً من Starter.
نصيحة
مقالات ذات صلة
هل كانت هذه الصفحة مفيدة؟