التقنية المالية والمدفوعات الأدلة التقنية

رسائل البنوك: ISO 8583 للبطاقات وISO 20022 للتحويلات

نفكك رسالة تفويض بطاقة بايتاً بايتاً، ونبني أمر تحويل بأسماء عربية ونتحقق منه بالمخطط الرسمي، ونرى ما يتعطل في كل معيار.

رسائل البنوك: ISO 8583 للبطاقات وISO 20022 للتحويلات

لنفترض أن بدر مهندس الواجهات الخلفية في شركة ناشئة تبني منصة للمتاجر الصغيرة: تستقبل المدفوعات بالبطاقات عبر جهاز نقطة بيع وصفحة دفع، وتصرف للمتاجر مستحقاتها كل أسبوع بتحويل بنكي. وفي أسبوع واحد وصلته وثيقتان من شريكين مختلفين: الأولى من المستحوذ Acquirer الذي يعالج البطاقات، فيها جداول طويلة لحقول مرقمة من 1 إلى 128 ورسائل اسمها 0100 و0110 و0420، والثانية من البنك الذي يحتفظ بحساب الشركة، فيها ملفات XML بأسماء مثل pain.001.001.09 وcamt.053. وكلا الشريكين يقول في أول صفحة إنه «يعتمد المعايير الدولية»، فوجد بدر نفسه أمام معيارين لا يشبه أحدهما الآخر في شيء.

والسؤال الذي طرحه على نفسه: لماذا يوجد معياران أصلاً؟ ومتى سأكتب أنا هذه الرسائل بنفسي، ومتى يكتبها غيري نيابة عني؟ وما الذي يمكن أن يتعطل فيها مع منتج عربي؟ وسوف نرافق بدر في هذا المقال وهو يفكك رسالة تفويض بطاقة حقيقية من المختبر بايتاً بايتاً، ثم يبني أمر تحويل بنكي بأسماء عربية ويتحقق منه بالمخطط الرسمي، ويجرب ما يحدث حين يخطئ.

وقد شرحنا في مقال عملية الدفع بالبطاقة طريق البطاقة من ضغطة «ادفع» إلى التسوية والاعتراض، وفي مقال منظومة المدفوعات في السعودية الأنظمة الوطنية مثل سريع ومدى وسداد، وهذا المقال يتعمق في الرسائل نفسها التي تحمل هذه العمليات بين الأطراف. وسوف نناقش في هذا المقال ما يلي:

  • لماذا تحتاج البنوك لغة مشتركة، ولماذا صار لها معياران.
  • ISO 8583: نوع الرسالة MTI، وخريطة الحقول Bitmap، والحقول الثابتة والمتغيرة، ولماذا لكل شبكة لهجتها.
  • تجربة عملية: نبني رسالة تفويض 0100 ببيانات اختبار ونفك خريطتها يدوياً وبمكتبة، ثم نجرب ثلاثة أعطال.
  • ISO 20022: عائلات الرسائل pain وpacs وcamt، وقراءة اسم الرسالة، والمخطط XSD.
  • تجربة عملية: أمر تحويل pain.001 بأسماء عربية نتحقق منه بالمخطط، ونكتشف حدوده الحقيقية.
  • العربية في المعيارين، والانتقال العالمي إلى ISO 20022، وما يلمسه فريق صغير فعلاً، وأخطاء شائعة وقائمة تحقق.

لماذا تحتاج البنوك لغة مشتركة؟

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

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

عالمان: على اليمين عالم البطاقات بمعيار ISO 8583 من جهاز نقطة البيع أو صفحة الدفع إلى المستحوذ إلى الشبكة إلى البنك المصدر برسائل مثل 0100 و0110 و0200 و0210 و0420، وعلى اليسار عالم التحويلات بين الحسابات بمعيار ISO 20022 من نظام الشركة بأمر pain.001 إلى بنكها ثم pacs.008 إلى البنك الآخر ثم pacs.002 لحالة التنفيذ ثم كشف الحساب camt.053 وcamt.054
البطاقات تتكلم ISO 8583، والتحويلات وكشوف الحسابات تتكلم ISO 20022

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

ISO 8583: رسالة البطاقة

رسالة ISO 8583 سلسلة من البايتات بلا أسماء ولا فواصل، تتكون كما يصفها ملخص المعيار المنشور من ثلاثة أجزاء: نوع الرسالة Message Type Indicator، ثم خريطة أو أكثر Bitmap تقول أي الحقول موجودة، ثم الحقول Data Elements نفسها بترتيب أرقامها. والمعيار له ثلاثة إصدارات رئيسية: 1987 و1993 و2003، والرقم الأول من نوع الرسالة يقول أي إصدار تستخدم، فـ 0 يعني 1987، وهو الأكثر انتشاراً في الأنظمة القائمة.

نوع الرسالة MTI

نوع الرسالة أربعة أرقام، لكل رقم معنى. فالرسالة 0100 تقرأ هكذا: الإصدار 0، أي 1987، ثم الفئة 1، أي رسالة تفويض Authorization تسأل هل يتوفر المال وتحجزه دون أن تخصمه، ثم الوظيفة 0، أي طلب، ثم المصدر 0، أي من المستحوذ. ورد المصدر عليها 0110، أي الرسالة نفسها بوظيفة 1، أي رد على الطلب. والفئة 2 للرسائل المالية Financial التي تخصم فعلاً مثل 0200، والفئة 4 للعكس Reversal مثل 0420 التي تلغي عملية سابقة حين لا يصل الرد أو يقطع الاتصال. وهذا التقسيم يطابق ما شرحناه في مقال البطاقة عن الفرق بين التفويض والتحصيل: رسالة تحجز، وأخرى تخصم، وثالثة تتراجع.

خريطة الحقول Bitmap

بعد نوع الرسالة تأتي الخريطة، وهي أذكى جزء في المعيار. فالمعيار يعرف 128 حقلاً ممكناً، لكن الرسالة الواحدة تحمل عادة عشرة أو عشرين منها، فبدل أن ترسل كل الحقول فارغة أو تكتب اسم كل حقل، ترسل 64 خانة ثنائية Bits، كل خانة تقابل حقلاً: 1 يعني موجود، و0 يعني غير موجود. والخانة الأولى خاصة: إذا كانت 1 فهناك خريطة ثانية للحقول من 65 إلى 128. فالخريطة تكلف 8 بايتات إذا أرسلت ثنائية، أو 16 حرفاً إذا كتبت بالأرقام الست عشرية كما في مواصفة المختبر، وتغني عن أسماء الحقول كلها.

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

حقول تستحق أن تعرفها

من الحقول المئة والثمانية والعشرين، يكفي فريقاً مثل فريق بدر أن يعرف قلة تظهر في كل تحقيق وكل مشكلة. وبحسب جدول حقول إصدار 1987 في الملخص نفسه:

  • الحقل 11، رقم التتبع System Trace Audit Number: ستة أرقام يضعها مرسل الطلب ويعود بها الرد، فيربط الرد بطلبه.
  • الحقل 37، الرقم المرجعي للاسترجاع Retrieval Reference Number: اثنا عشر حرفاً تعرف العملية عند الأطراف، وهو ما يطلبه البنك غالباً حين تسأله عن عملية بعينها.
  • الحقل 38، رمز الموافقة: ستة أحرف يعطيها البنك المصدر عند الموافقة، وتظهر عادة في إيصال الجهاز.
  • الحقل 39، رمز الرد Response Code: حرفان يقولان النتيجة، فـ 00 موافقة أو إتمام ناجح، و05 «لا تكرم» Do not honor، وهو رفض عام دون سبب محدد، و51 وغيرها لأسباب أخرى، والجدول الكامل في الملخص، وكل شبكة قد تضيف رموزها.
  • الحقل 55، بيانات الشريحة ICC data: حقل متغير يحمل بيانات EMV من شريحة البطاقة في عمليات الجهاز، بوسوم متعددة داخل الحقل نفسه، فهو رسالة صغيرة داخل الرسالة.

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

لماذا لكل شبكة لهجتها؟

المعيار يحدد الإطار، لكنه يترك مساحات واسعة للاستخدام الخاص: حقول محجوزة للاستخدام الوطني أو الخاص، وحقول يحدد كل طرف محتواها الداخلي، وخيارات في الترميز، فالخريطة قد ترسل ثنائية أو نصية، والأرقام قد ترسل نصاً أو بترميز مضغوط BCD. ولهذا تنشر كل شبكة بطاقات وكل مستحوذ مواصفته الخاصة المبنية على ISO 8583، وهي الوثيقة الطويلة التي وصلت بدر. والمكتبات مفتوحة المصدر تعكس هذا: مكتبة pyiso8583 التي سنستخدمها لا تفترض لهجة واحدة، بل تطلب منك مواصفة Specification تعرف فيها طول كل حقل ونوعه وترميزه، وتأتي معها مواصفتان افتراضيتان للتجربة فقط.

وفهم بدر من هذا أن جملة «نحن نستخدم ISO 8583» لا تكفي ليعرف شكل الرسالة، تماماً كما أن «نحن نستخدم JSON» لا تكفي ليعرف أسماء الحقول في واجهة برمجية. فالمعيار يعطي القواعد العامة، والمواصفة الخاصة بالشريك تعطي التفاصيل التي يتعطل بدونها كل شيء. ولهذا كان أول ما طلبه بدر من المستحوذ ليس وثيقة المعيار، بل مواصفته الخاصة مع أمثلة رسائل حقيقية من بيئة الاختبار، طلباً وردا، لأن المثال الواحد الصحيح يحسم أسئلة لا تحسمها عشر صفحات من الجداول: هل الخريطة نصية أم ثنائية، وهل الأرقام نص أم مضغوطة، وكيف تكتب الأطوال المتغيرة. ومع هذه الأمثلة يستطيع أن يكتب مواصفة في مكتبة مثل pyiso8583 ويتحقق أنها تفك رسائل الشريك كما يجب قبل أي اتصال حقيقي.

تجربة عملية: رسالة تفويض بايتاً بايتاً

أراد بدر أن يرى رسالة حقيقية لا جدولاً، فبنى رسالة تفويض بمكتبة pyiso8583 الإصدار 4.0.1، وهي مكتبة Python بترخيص MIT نشر إصدارها الأخير في أغسطس 2025، بالمواصفة الافتراضية النصية default_ascii التي تكتب الخريطة بالأرقام الست عشرية فيسهل قراءتها. والبيانات كلها بيانات اختبار: رقم البطاقة هو رقم الاختبار المنشور في توثيق Stripe للاختبار، ولا يمثل أي بطاقة حقيقية، والمبلغ 150 ريالاً بالعملة رقم 682 وهو الرمز الرقمي للريال السعودي في ISO 4217:

# ISO 8583 by hand and with pyiso8583: an authorization request (0100) and its response (0110), test data only.
import copy
import iso8583
from iso8583.specs import default_ascii as spec

req = {
    "t": "0100",                     # MTI: version 0 (1987), class 1 (authorization), function 0 (request), origin 0
    "2": "4242424242424242",         # Stripe's published test card number, never a real card
    "3": "000000",                   # processing code: purchase
    "4": "000000015000",             # amount in the smallest unit: 150.00
    "7": "1027083000",               # transmission date and time MMDDhhmmss (UTC)
    "11": "000123",                  # system trace audit number
    "41": "TERM0001",                # terminal id
    "42": "ARABROOTLAB0001",         # merchant id, 15 characters
    "49": "682",                     # currency: ISO 4217 numeric for SAR
}

def main():
    raw, encoded = iso8583.encode(copy.deepcopy(req), spec)
    print("bytes:", len(raw))
    print("raw  :", raw.decode("ascii"))
    print("MTI  :", raw[:4].decode(), "| bitmap hex:", raw[4:20].decode())

    bitmap = int(raw[4:20], 16)
    fields = [i + 1 for i in range(64) if bitmap >> (63 - i) & 1]
    print("fields from the bitmap by hand:", fields)

    dec, _ = iso8583.decode(raw, spec)
    print("fields from pyiso8583        :", sorted(int(k) for k in dec if k.isdigit() and k not in ("p",)))

    resp = {"t": "0110", "2": dec["2"], "3": dec["3"], "4": dec["4"], "7": dec["7"], "11": dec["11"],
            "38": "A1B2C3", "39": "00", "41": dec["41"], "42": dec["42"], "49": dec["49"]}
    raw2, _ = iso8583.encode(resp, spec)
    d2, _ = iso8583.decode(raw2, spec)
    print("response MTI", d2["t"], "| auth code", d2["38"], "| response code", d2["39"], "| same STAN", d2["11"] == dec["11"])


if __name__ == "__main__":
    main()

ولماذا بهذه الطريقة؟ نكتب الرسالة كقاموس Python فيه رقم كل حقل وقيمته، فتبني المكتبة الخريطة وحدها من الحقول الموجودة، ثم نطبع الرسالة الخام كما ستنتقل على الشبكة. وبعد ذلك نفك الخريطة يدوياً بسطرين: نحول الأرقام الست عشرية إلى عدد صحيح، ونمر على خاناته الأربع والستين من اليسار، فكل خانة قيمتها 1 هي حقل موجود. ونقارن النتيجة بما تقوله المكتبة، فإذا اتفقتا عرفنا أننا فهمنا الخريطة. ثم نبني الرد 0110 بالحقول التي يجب أن تعود كما هي، ونضيف إليها رمز الموافقة في الحقل 38 ورمز الرد في الحقل 39. وشغلناها في Container مؤقت من python:3.13-slim، بـ Python 3.13.17:

docker run --rm -v "$PWD":/w -w /w python:3.13-slim sh -c \
  "pip install -q pyiso8583==4.0.1 && python iso8583_lab.py"
bytes: 98
raw  : 01007220000000C080001642424242424242420000000000000150001027083000000123TERM0001ARABROOTLAB0001682
MTI  : 0100 | bitmap hex: 7220000000C08000
fields from the bitmap by hand: [2, 3, 4, 7, 11, 41, 42, 49]
fields from pyiso8583        : [2, 3, 4, 7, 11, 41, 42, 49]
response MTI 0110 | auth code A1B2C3 | response code 00 | same STAN True

في المخرج أعلاه لاحظ التالي:

  • الرسالة كلها 98 بايتاً: نوع الرسالة 4، والخريطة 16، والباقي حقول متلاصقة دون أي فاصل. وفيها رقم البطاقة كاملاً كما هو، ولهذا تعامل هذه الرسائل بمعايير حماية بيانات البطاقات PCI DSS التي شرحناها في مقال البطاقة.
  • الخريطة 7220000000C08000 فكها الكود اليدوي والمكتبة إلى الحقول نفسها: 2 و3 و4 و7 و11 و41 و42 و49، وهي بالضبط ما وضعناه.
  • بعد الخريطة مباشرة الرقمان 16، وهما طول رقم البطاقة في الحقل المتغير 2، ثم الأرقام الستة عشر نفسها.
  • في الرد رمز الرد 00، وهو في جدول رموز الإصدار 1987 يعني الموافقة، ورقم التتبع STAN في الحقل 11 هو نفسه في الطلب والرد، وهذا ما يربط الرد بطلبه حين تتطاير آلاف الرسائل في الثانية.

والصورتان التاليتان تشرحان الرسالة نفسها، الأولى حقلاً حقلاً، والثانية خانة خانة في الخريطة:

عشر بطاقات لأجزاء رسالة التفويض الحقيقية من المختبر: MTI بقيمة 0100، وBitmap بقيمة 7220000000C08000، والحقل 2 رقم البطاقة 16 ثم 4242424242424242، والحقل 3 نوع العملية 000000، والحقل 4 المبلغ 000000015000 أي 150.00، والحقل 7 وقت الإرسال، والحقل 11 رقم التتبع 000123، والحقل 41 الجهاز TERM0001، والحقل 42 التاجر ARABROOTLAB0001، والحقل 49 العملة 682 أي الريال
98 بايتاً: لا أسماء ولا فواصل، والمعنى كله في الترتيب والأطوال
فك الخريطة 7220000000C08000: كل رقم ست عشري أربع خانات ثنائية، فالرقم 7 يعطي الحقول 2 و3 و4، والرقم 2 يعطي الحقل 7، والرقم 2 التالي يعطي الحقل 11، والرقم C يعطي الحقلين 41 و42، والرقم 8 يعطي الحقل 49، والخانة الأولى صفر فلا توجد خريطة ثانية
من 16 رقماً إلى قائمة حقول، بالنتيجة نفسها يدوياً وبالمكتبة

وفي أثناء التجربة وقع بدر في خطأ صغير لا علاقة له بالمعيار: كتب سكربت التجارب التالية يستورد القاموس req من ملف التجربة الأولى، فطبعت كل مخرجات التجربة الأولى مرة ثانية قبل التجارب الجديدة، لأن Python ينفذ كل كود الملف حين يستورده. فنقل الطباعة إلى دالة main لا تعمل إلا حين يشغل الملف مباشرة، كما ترى في الكود أعلاه.

ماذا لو؟ ثلاثة أعطال في رسالة واحدة

ثم جرب بدر ثلاثة أشياء يتوقع أن تحدث في الحياة الحقيقية، بسكربت يستورد الرسالة نفسها ويغير فيها شيئاً واحداً كل مرة:

import copy
import iso8583
from iso8583.specs import default_ascii as spec
from iso8583_lab import req

print("what if: an Arabic merchant name in field 43 (name/location, 40 characters)")
m = copy.deepcopy(req); m["43"] = "متجر عرب رووت التجريبي"
try:
    iso8583.encode(m, spec)
except Exception as e:
    print("  ", type(e).__name__, "-", e)

print("what if: the message is cut short in transit")
raw, _ = iso8583.encode(copy.deepcopy(req), spec)
try:
    iso8583.decode(raw[:-5], spec)
except Exception as e:
    print("  ", type(e).__name__, "-", e)

print("what if: we read it with a binary-bitmap spec")
from iso8583.specs import default as bin_spec
try:
    iso8583.decode(raw, bin_spec)
except Exception as e:
    print("  ", type(e).__name__, "-", e)
what if: an Arabic merchant name in field 43 (name/location, 40 characters)
   EncodeError - Failed to encode field, invalid data: field 43
what if: the message is cut short in transit
   DecodeError - Field data is 13 bytes, expecting 15: field 42 pos 80
what if: we read it with a binary-bitmap spec
   DecodeError - Field data is 68 bytes, larger than maximum 37: field 35 pos 95
  • اسم تاجر عربي في الحقل 43، وهو حقل اسم التاجر وموقعه بطول 40 حرفاً: رفضت المكتبة ترميزه. والسبب أن الحقل في هذه المواصفة نصي بترميز ASCII، والحروف العربية ليست فيه. وهذا ليس عيباً في المكتبة، بل طبيعة المعيار القديم: الحقول النصية فيه مصممة لحروف لاتينية وأرقام ورموز محدودة. ولهذا ترى في كشف بطاقتك أسماء متاجر محلية مكتوبة بحروف لاتينية مختصرة، فهي تمر في هذه الرسائل بهذا الشكل.
  • رسالة قطعت خمسة بايتات من آخرها: عرفت المكتبة المشكلة بدقة، فالحقل 42 يجب أن يكون 15 بايتاً ووجدت 13 فقط عند الموضع 80. وهذا ما تفعله الأنظمة الحقيقية: ترفض الرسالة ولا تحاول تخمين الباقي، لأن التخمين في رسالة مالية أسوأ من الرفض.
  • قراءة الرسالة بمواصفة تتوقع خريطة ثنائية: وهنا المثال الأخطر. فالمكتبة قرأت أول 8 بايتات من الخريطة النصية كأنها خريطة ثنائية، فخرجت بقائمة حقول مختلفة تماماً، منها الحقل 35، ثم حاولت قراءته فوجدت طولاً لا يعقل. ولو كانت الأطوال صدفة مقبولة، لقرأت الرسالة «بنجاح» وأعطت قيماً خاطئة. وهذا بالضبط ما يحدث حين يختلف طرفان في تفصيل واحد من المواصفة: البايتات نفسها، والمعنى مختلف.

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

حين لا يصل الرد: العكس Reversal

بقي عند بدر سؤال من مقال البطاقة: ماذا يحدث إذا أرسل الجهاز طلب التفويض 0100 ولم يصل الرد خلال المهلة؟ الطلب ربما وصل ووافق البنك وحجز المبلغ، وربما ضاع في الطريق، ولا يعرف الجهاز أيهما. وهنا تأتي فئة الرسائل الرابعة: رسالة العكس 0400 أو 0420، وهي كما يلخصها جدول الفئات «تعكس أثر تفويض سابق». فيرسلها المستحوذ ليلغي الحجز إن كان حدث، والبنك المصدر يعرف بالرقم المرجعي هل عنده تفويض بهذا الرقم فيلغيه، أو لا شيء عنده فلا يفعل شيئاً. ولاحظ أن العكس آمن في الحالتين، ولهذا هو الحل المعيار لعدم اليقين.

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

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

ISO 20022: رسالة التحويل

ISO 20022 فكرة مختلفة من الأساس. فهو ليس رسالة واحدة بحقول مرقمة، بل منهجية لتعريف رسائل مالية بنموذج بيانات مشترك، تنتج منه رسائل XML لكل غرض، لكل منها مخطط XSD يحدد عناصره وأنواعها وحدودها. والرسالة تحمل أسماء عناصرها، فمن يقرأ <InstdAmt Ccy="SAR">1500.00</InstdAmt> يفهم أنه المبلغ المطلوب بالريال دون أي جدول خارجي. والثمن أن الرسالة أكبر بكثير: أمر تحويل واحد في تجربتنا 1,461 بايتاً، أي قرابة خمسة عشر ضعف رسالة التفويض.

عائلات الرسائل وقراءة الاسم

تقسم رسائل المدفوعات في ISO 20022 إلى مجالات عمل، أهمها لفريق مثل فريق بدر ثلاثة، والصورة التالية تجمعها:

ثلاث عائلات: pain اختصار Payments Initiation من العميل إلى بنكه ومثالها pain.001 أمر تحويل، وpacs اختصار Payments Clearing and Settlement بين البنوك ومثالها pacs.008 تحويل وpacs.002 حالة، وcamt اختصار Cash Management من البنك إلى العميل ومثالها camt.053 كشف وcamt.054 إشعار، وتحتها شرح اسم pain.001.001.09: مجال العمل ثم رقم الرسالة ثم المتغير ثم الإصدار
pain من الشركة إلى بنكها، وpacs بين البنوك، وcamt من البنك إلى الشركة
  • pain: رسائل بدء الدفع من العميل إلى بنكه، وأشهرها pain.001 أمر التحويل، وهو ما ترسله الشركة حين تصرف رواتب أو مستحقات موردين دفعة واحدة.
  • pacs: رسائل المقاصة والتسوية بين البنوك، مثل pacs.008 للتحويل نفسه وpacs.002 لحالته، وهذه يرسلها البنك لا الشركة.
  • camt: رسائل إدارة النقد من البنك إلى العميل، مثل camt.053 كشف الحساب اليومي وcamt.054 إشعار الحركة، وهي ما يقرؤه نظام الشركة ليعرف ما دخل وخرج، وموضوع مقال المطابقة القادم في هذه المرحلة.

واسم الرسالة نفسه يقرأ بأربعة أجزاء كما يظهر في فضاء الأسماء Namespace داخل المخطط: pain.001.001.09 يعني مجال pain، والرسالة 001، والمتغير 001، والإصدار 09. وهذا الرقم الأخير مهم عملياً: الإصدارات تختلف في عناصرها وحدودها، والبنك يحدد أي إصدار يقبل، فالملف الصحيح بالإصدار 09 قد يرفضه بنك يتوقع الإصدار 03. فأول سؤال سأله بدر للبنك لم يكن عن الحقول، بل: أي إصدار من أي رسالة، وأين مخططه ودليل استخدامه؟

تجربة عملية: أمر تحويل بأسماء عربية

أراد بدر أن يكتب ملف صرف المستحقات بنفسه ويتحقق منه قبل أن يرسله للبنك، لا أن يكتشف أخطاءه من رفض البنك. واحتاج لذلك المخطط الرسمي للرسالة. ولم تتح صفحات موقع المعيار التنزيل الآلي من بيئة المختبر، لكن مكتبة sepaxml 2.7.0 تحمل معها ملفات XSD لعدة إصدارات من pain.001، وفي رأس الملف الذي استخدمناه أنه مولد بمحرر المعايير Standards Editor لإصدار ISO 20022، وفضاء أسمائه urn:iso:std:iso:20022:tech:xsd:pain.001.001.09، فهو المخطط العام للرسالة لا نسخة مقيدة. فكتب سكربتاً يبني الأمر بمكتبة lxml ويتحقق منه ثم يقرؤه:

# ISO 20022: build a pain.001.001.09 credit transfer with Arabic names, validate it against the ISO-generated XSD,
# then read it back the way a bank system would. Example data only.
import os, sys
from lxml import etree
import sepaxml

NS = "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"
XSD = os.path.join(os.path.dirname(sepaxml.__file__), "schemas", "pain.001.001.09.xsd")
schema = etree.XMLSchema(etree.parse(XSD))

def iban_sa(bban):                      # build an IBAN with a valid check (ISO 13616 mod 97), example only
    s = bban + "SA00"
    n = int("".join(str(int(c, 36)) for c in s))
    return f"SA{98 - n % 97:02d}{bban}"

def el(parent, tag, text=None, **attrs):
    e = etree.SubElement(parent, f"{{{NS}}}{tag}", **attrs)
    if text is not None:
        e.text = text
    return e

def build(creditor_name):
    doc = etree.Element(f"{{{NS}}}Document", nsmap={None: NS})
    root = el(doc, "CstmrCdtTrfInitn")
    gh = el(root, "GrpHdr")
    el(gh, "MsgId", "ARABROOT-2026-10-27-001"); el(gh, "CreDtTm", "2026-10-27T09:00:00")
    el(gh, "NbOfTxs", "1"); el(gh, "CtrlSum", "1500.00")
    el(el(gh, "InitgPty"), "Nm", "شركة عرب رووت التجريبية")
    pi = el(root, "PmtInf")
    el(pi, "PmtInfId", "PAYROLL-OCT"); el(pi, "PmtMtd", "TRF")
    el(el(pi, "ReqdExctnDt"), "Dt", "2026-10-28")
    el(el(pi, "Dbtr"), "Nm", "شركة عرب رووت التجريبية")
    el(el(el(pi, "DbtrAcct"), "Id"), "IBAN", iban_sa("8000000000000000000001"))
    el(el(el(pi, "DbtrAgt"), "FinInstnId"), "BICFI", "TESTSARIXXX")
    tx = el(pi, "CdtTrfTxInf")
    el(el(tx, "PmtId"), "EndToEndId", "INV-2026-0042")
    el(el(tx, "Amt"), "InstdAmt", "1500.00", Ccy="SAR")
    el(el(tx, "Cdtr"), "Nm", creditor_name)
    el(el(el(tx, "CdtrAcct"), "Id"), "IBAN", iban_sa("2000000000000000000042"))
    el(el(tx, "RmtInf"), "Ustrd", "فاتورة INV-2026-0042 خدمات استضافة أكتوبر")
    return doc

if __name__ == "__main__":
    doc = build("مؤسسة المثال للخدمات التقنية")
    xml = etree.tostring(doc, xml_declaration=True, encoding="UTF-8", pretty_print=True)
    open("pain001.xml", "wb").write(xml)
    print("bytes:", len(xml), "| valid against pain.001.001.09.xsd:", schema.validate(doc))
    ns = {"p": NS}
    for path in ["//p:GrpHdr/p:MsgId", "//p:DbtrAcct//p:IBAN", "//p:EndToEndId", "//p:InstdAmt", "//p:Cdtr/p:Nm", "//p:Ustrd"]:
        node = doc.xpath(path, namespaces=ns)[0]
        print(f"{path:22} {node.text}  {dict(node.attrib) or ''}")
    for name in ("أ" * 140, "أ" * 141):
        d = build(name)
        ok = schema.validate(d)
        print(f"creditor name {len(name)} chars, {len(name.encode())} bytes -> valid {ok}", "" if ok else schema.error_log.last_error.message[:120])

ولماذا بهذه الطريقة؟ ننشئ كل عنصر داخل فضاء أسماء الرسالة، لأن المخطط يرفض العناصر التي لا تنتمي إليه حتى لو تطابقت أسماؤها. والآيبان في التجربة مولد برقم تحقق صحيح بطريقة mod 97 المعروفة، حتى لا يكون خطأ التحقق سبب الرفض، لكنه رقم مثال لا يمثل حساباً حقيقياً، ومعرف البنك TESTSARIXXX مثال كذلك. ونقرأ الرسالة بعد بنائها بتعبيرات XPath كما يقرؤها نظام البنك. ثم نجرب حدود اسم المستفيد. والمخرج:

lxml 6.1.3
bytes: 1461 | valid against pain.001.001.09.xsd: True
//p:GrpHdr/p:MsgId     ARABROOT-2026-10-27-001
//p:DbtrAcct//p:IBAN   SA258000000000000000000001
//p:EndToEndId         INV-2026-0042
//p:InstdAmt           1500.00  {'Ccy': 'SAR'}
//p:Cdtr/p:Nm          مؤسسة المثال للخدمات التقنية
//p:Ustrd              فاتورة INV-2026-0042 خدمات استضافة أكتوبر
creditor name 140 chars, 280 bytes -> valid True
creditor name 141 chars, 282 bytes -> valid False Element '{urn:iso:std:iso:20022:tech:xsd:pain.001.001.09}Nm': [facet 'maxLength'] The value has a length of '141'; this

وهذا جزء من الملف الذي خرج، مختصراً:

<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09">
  <CstmrCdtTrfInitn>
    <GrpHdr>
      <MsgId>ARABROOT-2026-10-27-001</MsgId>
      <CreDtTm>2026-10-27T09:00:00</CreDtTm>
      <NbOfTxs>1</NbOfTxs>
      <CtrlSum>1500.00</CtrlSum>
      ...
    <CdtTrfTxInf>
      <PmtId><EndToEndId>INV-2026-0042</EndToEndId></PmtId>
      <Amt><InstdAmt Ccy="SAR">1500.00</InstdAmt></Amt>
      <Cdtr><Nm>مؤسسة المثال للخدمات التقنية</Nm></Cdtr>
      <CdtrAcct><Id><IBAN>SA...</IBAN></Id></CdtrAcct>
      <RmtInf><Ustrd>فاتورة INV-2026-0042 خدمات استضافة أكتوبر</Ustrd></RmtInf>
    </CdtTrfTxInf>

في المخرج أعلاه لاحظ التالي:

  • الملف صحيح بحسب المخطط، وفيه أسماء عربية في الشركة المرسلة والمستفيد ونص الحوالة، فـ ISO 20022 يكتب بترميز UTF-8 ولا يمنع العربية في هذه العناصر.
  • المرجع من البداية إلى النهاية EndToEndId قيمته رقم الفاتورة، وهذا العنصر يعود في رسائل البنك وفي كشف الحساب، فهو ما يربط الحركة في الكشف بالفاتورة في نظامك، وسنعود إليه في مقال المطابقة.
  • المبلغ نص عشري مع رمز العملة صفةً Ccy="SAR"، لا رقماً بالهللات كما في ISO 8583، فلا يحتاج من يقرؤه أن يعرف عدد الخانات العشرية مسبقاً.

ماذا لو كان الاسم أطول؟ وما الذي اكتشفناه

هنا وقع بدر في خطأ مفيد. فقد كان يتذكر أن أسماء الأطراف في هذه الرسائل حدها 70 حرفاً، فكتب التجربة الأولى باسم من 70 حرفاً ثم 71، متوقعاً أن يرفض الثاني. فقبل المخطط الاسمين. ففتح المخطط نفسه، فوجد أن عنصر الاسم في نوع الطرف PartyIdentification135 في هذا الإصدار من نوع Max140Text، أي حده 140 حرفاً، وأن حد 70 الذي يتذكره نوع آخر يستخدم في عناصر أخرى أو إصدارات أخرى. فغير التجربة إلى 140 و141، فكان الناتج كما رأيت: 140 حرفاً صحيحة، و141 مرفوضة برسالة maxLength تذكر الطول بالضبط.

والملاحظة الأهم في هذين السطرين أن 140 حرفاً عربياً تساوي 280 بايتاً في UTF-8، ومع ذلك قبلها المخطط، لأن حدود الطول في XML Schema تعد الحروف لا البايتات. وهذا يختلف عن أدوات كثيرة تعد البايتات فتظلم النص العربي. لكن بدر كتب ملاحظة ثانية لا تقل أهمية: أن يقبل المخطط العام شيئاً لا يعني أن يقبله كل بنك أو كل نظام في الطريق. فالبنوك وأنظمة المدفوعات تنشر أدلة استخدام Usage Guidelines تقيد المخطط العام، فقد تقصر الأطوال أو تحدد مجموعة حروف أضيق أو تجعل عناصر اختيارية إلزامية. فالمخطط يجيب «هل الملف سليم البنية؟»، ودليل البنك يجيب «هل سيقبله هذا البنك؟»، ويحتاج بدر الاثنين.

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

# What the schema checks and what it does not: a wrong control sum, a bad IBAN check digit, a wrong currency code.
import copy
from iso20022_lab import build, schema, NS
ns = {"p": NS}

def variant(label, path, value):
    doc = build("مؤسسة المثال للخدمات التقنية")
    doc.xpath(path, namespaces=ns)[0].text = value
    ok = schema.validate(doc)
    print(f"{label:38} valid {ok}", "" if ok else "| " + schema.error_log.last_error.message[:90])

variant("control sum 1600.00 for a 1500.00 payment", "//p:GrpHdr/p:CtrlSum", "1600.00")
variant("IBAN with a wrong check digit", "//p:CdtrAcct//p:IBAN", "SA992000000000000000000042")
variant("IBAN in lower case", "//p:CdtrAcct//p:IBAN", "sa992000000000000000000042")
doc = build("مؤسسة المثال للخدمات التقنية")
doc.xpath("//p:InstdAmt", namespaces=ns)[0].set("Ccy", "XYZ")
print(f"{'currency code XYZ':38} valid {schema.validate(doc)}")
doc.xpath("//p:InstdAmt", namespaces=ns)[0].set("Ccy", "sar")
ok = schema.validate(doc)
print(f"{'currency code sar':38} valid {ok}", "" if ok else "| " + schema.error_log.last_error.message[:90])
control sum 1600.00 for a 1500.00 payment valid True
IBAN with a wrong check digit          valid True
IBAN in lower case                     valid False | Element '{urn:iso:std:iso:20022:tech:xsd:pain.001.001.09}IBAN': [facet 'pattern'] The valu
currency code XYZ                      valid True
currency code sar                      valid False | Element '{urn:iso:std:iso:20022:tech:xsd:pain.001.001.09}InstdAmt', attribute 'Ccy': [face

والنتيجة تستحق التوقف. فمجموع التحقق 1,600 لحوالة قيمتها 1,500 مر، وآيبان برقم تحقق خاطئ مر، ورمز عملة غير موجود XYZ مر. أما الآيبان بحروف صغيرة ورمز العملة بحروف صغيرة فرفضا. والسبب أن المخطط يتحقق من الشكل لا من المعنى: الآيبان يجب أن يطابق نمطاً من حرفين كبيرين ورقمين ثم حروف وأرقام، لكن المخطط لا يحسب رقم التحقق، والعملة ثلاثة أحرف كبيرة لكن المخطط لا يعرف قائمة العملات، والمجموع رقم عشري صحيح الشكل لكن المخطط لا يجمع المبالغ. وهذا يشبه ما رأيناه مع أدوات فحص المواقع حين قبلت رمز لغة صحيح الشكل خاطئ المعنى.

فأضاف بدر إلى نظامه طبقة ثانية بعد المخطط: يجمع المبالغ ويقارنها بالمجموع، ويتحقق من رقم الآيبان بطريقة mod 97 نفسها التي ولدناه بها، ويتأكد أن العملة من قائمة يعرفها النظام. فالمخطط خط الدفاع الأول ضد ملف معيب البنية، وقواعد العمل خط الدفاع الثاني ضد ملف سليم البنية خاطئ المضمون، والبنك نفسه يطبق قواعده الخاصة فوق الاثنين.

خمس تجارب ونتائجها الحقيقية: اسم تاجر عربي في الحقل 43 أعطى EncodeError، ورسالة قطعت أثناء النقل أعطت DecodeError لأن الحقل 42 ثلاثة عشر بايتاً بدل خمسة عشر، وقراءة بمواصفة خريطة ثنائية أعطت DecodeError في الحقل 35، واسم مستفيد عربي من 140 حرفاً أي 280 بايتاً صحيح، واسم من 141 حرفاً مرفوض بسبب maxLength
مخرج حقيقي: ISO 8583 لا يقبل العربية في هذا الحقل، وISO 20022 يعدها بالحروف

ما الذي تحمله الرسالة الأغنى فعلاً؟

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

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

ولاحظ الفرق في فلسفة التحقق بين المعيارين. ففي ISO 8583 لا يعرف المستقبل أن الرسالة خاطئة إلا حين يختل ما بعدها، كما رأينا في العطل الثالث. أما في ISO 20022 فالمخطط يقول قبل الإرسال أي عنصر غير صحيح، وبأي قيد، وفي أي سطر، كما قال لنا عن العنصر Nm بالضبط. وهذا يجعل التحقق خطوة يضعها بدر في نظامه قبل كل إرسال، فيكتشف الخطأ في بيئته بدل أن يكتشفه من رفض البنك بعد يوم.

العربية في المعيارين

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

وأضاف بدر قاعدة رابعة بعد أن رأى التجارب: أن يحفظ في قاعدة بيانات نظامه كل رسالة يرسلها وكل رد يستقبله، مع المرجع والوقت والحالة، لكن دون رقم البطاقة الكامل في رسائل البطاقات. فحين يسأل أحد بعد شهر لماذا لم تصل حوالة أو لماذا رفضت عملية، يجد الجواب في سجل محفوظ لا في ذاكرة أحد، وهذا السجل نفسه هو ما يحتاجه للمطابقة في المقال التالي.

الانتقال العالمي إلى ISO 20022

لم يعد ISO 20022 معياراً بين معايير، بل صار لغة أنظمة الدفع الكبرى. فالبنك المركزي الأوروبي يذكر في صفحة نظام T2 أنه أطلق في مارس 2023 بديلاً لنظام TARGET2، وأنه يستخدم ISO 20022 كبقية خدمات منظومة TARGET. والاحتياطي الفيدرالي الأمريكي يذكر في مركز تطبيق ISO 20022 لخدمة Fedwire أن انتقالها إلى المعيار جعل صيغ رسائلها متوافقة مع أنظمة الدفع الكبرى الأخرى في العالم. وفي السعودية تذكر صفحة نظام المدفوعات الفورية سريع لدى البنك المركزي من مزاياه التوافق مع المعايير الدولية والتشغيل البيني وفقاً لـ ISO 20022، كما ذكرنا في مقال منظومة المدفوعات.

وماذا عن البطاقات؟ ISO 8583 باق فيها، لأن البنية التحتية للبطاقات من أجهزة وشبكات ومحولات مبنية عليه منذ عقود، ولأن رسالة التفويض الصغيرة السريعة ما زالت تؤدي غرضها. ولا نملك مصدراً رسمياً لموعد انتقال شبكات البطاقات إلى ISO 20022، فلا نتوقعه. والنتيجة العملية لبدر أن المعيارين سيبقيان معه سنوات: البطاقات بلغتها القديمة، والتحويلات وكشوف الحسابات باللغة الجديدة.

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

ما الذي يلمسه فريق صغير فعلاً؟

وصل بدر في النهاية إلى الجواب الذي غير خطته. ففي البطاقات، شركته لا تتصل بالشبكات ولا تكتب رسائل ISO 8583، بل تتصل بمزود الدفع أو المستحوذ عبر واجهة برمجية حديثة بـ JSON، والمزود هو من يترجم إلى ISO 8583 ويتعامل مع الشبكات واعتماداتها. والاتصال المباشر بالشبكة بهذه الرسائل يحتاج تراخيص واعتمادات وبنية أمنية لا تفيد شركة ناشئة. لكن فهم الرسالة ما زال مفيداً له: رمز الرد الذي يعيده المزود في واجهته أصله غالباً رمز الحقل 39، واسم التاجر في كشف العميل قيده الحقل 43، ورقم التتبع الذي يطلبه الدعم الفني حين يحقق في عملية هو الحقل 11.

أما في التحويلات فالأمر مختلف. فإذا أرادت الشركة صرف مستحقات المتاجر دفعة واحدة من حسابها، فقد يطلب البنك ملف pain.001 ترفعه الشركة في بوابته أو ترسله عبر قناة متصلة، وقد يعطيها كشف الحساب اليومي بصيغة camt.053. وهنا يكتب بدر الملفات ويقرؤها بنفسه، فيحتاج المخطط ودليل الاستخدام والاختبار قبل الإنتاج. وتختلف القنوات المتاحة بين البنوك، فاسأل بنكك عما يقدمه فعلاً، ولا تبن على افتراض. وكل صرف من حساب الشركة يجب أن يقابله قيد في دفتر الحسابات، كما شرحنا في دراسة حالة المحفظة الرقمية ودفتر القيد المزدوج.

كيف تختبر تكاملاً مالياً دون بيانات حقيقية؟

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

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

أخطاء شائعة Common Mistakes

  • معاملة «ISO 8583» كأنه مواصفة كاملة: كل شبكة ومستحوذ لهما لهجة، والاختلاف في طول حقل واحد أو ترميز الخريطة يفسد الرسالة كلها، كما رأينا.
  • تسجيل الرسائل الخام في السجلات: رسالة التفويض تحمل رقم البطاقة كاملاً، فطباعتها في سجل التطبيق تدخل السجل كله في نطاق PCI DSS.
  • الاعتماد على المخطط العام وحده: المخطط يقول هل البنية صحيحة، ودليل استخدام البنك يقول هل سيقبلها، والحدود فيه قد تكون أضيق، ولا يتحقق المخطط من المجموع ولا من رقم تحقق الآيبان ولا من وجود العملة، كما رأينا في التجربة.
  • الخلط بين الإصدارات: pain.001.001.03 وpain.001.001.09 رسالتان بمخططين مختلفين، فاسأل البنك عن الإصدار قبل أن تكتب سطراً.
  • إهمال المرجع من البداية إلى النهاية: إذا لم تضع في EndToEndId مرجعاً تعرفه، فلن تستطيع ربط الحركة في كشف الحساب بما في نظامك آلياً.
  • افتراض أن العربية ستمر في كل مكان: رسائل البطاقات في المواصفة التي جربناها لا تحملها في اسم التاجر، والتحويلات تحملها بحسب دليل البنك والنظام.
  • حساب الحدود بالبايتات أو بالحروف دون سؤال: المخطط يعد الحروف، وبعض الأنظمة في الطريق قد تعد البايتات.

قائمة تحقق قبل أي تكامل مع بنك أو مستحوذ

السؤاللماذا
هل سأكتب الرسائل بنفسي، أم يكتبها مزود نيابة عني؟في البطاقات غالباً المزود، وفي ملفات البنك غالباً أنت
ما المعيار والإصدار والرسالة بالضبط؟pain.001.001.09 غير pain.001.001.03
أين المخطط XSD ودليل الاستخدام؟المخطط للبنية، والدليل لحدود البنك وإلزاماته
كيف تعامل العربية، وكيف تعد الأطوال؟ISO 8583 لم يقبلها في الحقل 43، والمخطط يعد الحروف
ما المرجع الذي يعود في الرد وفي كشف الحساب؟STAN في البطاقات، وEndToEndId في التحويلات
أين بيئة الاختبار، وما بيانات الاختبار المسموحة؟لا بطاقات حقيقية ولا حسابات حقيقية في الاختبار
هل نتحقق من كل ملف بالمخطط قبل إرساله؟الرفض في نظامك أرخص من الرفض في البنك
هل السجلات خالية من بيانات البطاقات؟الرسالة الخام تحمل رقم البطاقة كاملاً

الخلاصة

  • ISO 8583 رسالة البطاقات: صغيرة وسريعة، معناها في الترتيب والأطوال، ونوع الرسالة MTI والخريطة Bitmap مفتاحا قراءتها، ولكل شبكة لهجتها.
  • رسالة التفويض في المختبر كانت 98 بايتاً، وفكت خريطتها 7220000000C08000 يدوياً وبالمكتبة إلى الحقول نفسها.
  • خطأ في طول حقل أو ترميز الخريطة يجعل البايتات نفسها تعني شيئاً آخر، والحقل النصي في المواصفة التي جربناها رفض الحروف العربية.
  • ISO 20022 رسائل XML بأسماء عناصر ومخطط XSD، بعائلات pain وpacs وcamt، وأمر التحويل في المختبر كان 1,461 بايتاً بأسماء عربية صحيحة بحسب المخطط.
  • المخطط يتحقق من الشكل لا من المعنى: مجموع خاطئ وآيبان برقم تحقق خاطئ وعملة غير موجودة مرت كلها، فتحتاج قواعد عمل في نظامك بعده.
  • حد اسم الطرف في pain.001.001.09 هو 140 حرفاً لا 70، والمخطط يعد الحروف: 140 حرفاً عربياً أي 280 بايتاً مقبولة، و141 مرفوضة.
  • أنظمة الدفع الكبرى انتقلت إلى ISO 20022، ومنها سريع في السعودية بحسب صفحته لدى البنك المركزي، والبطاقات باقية على ISO 8583.

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

وبقية مقالات قسم التقنية المالية والمدفوعات:

دليلكيف يعمل الدفع بالبطاقة؟ التفويض Authorization والتسويةمن ضغطة «ادفع» إلى آخر اعتراض: التفويض و3D Secure والتحصيل والتسوية والاسترداد والاعتراض، مع مختبر لأحداث مزود الدفع ومطابقتها.دليلمنظومة المدفوعات في السعودية: سريع وSARIE ومدى وسدادالأنظمة الوطنية للمدفوعات في المملكة كما تصفها مصادر البنك المركزي، وما يعنيه كل نظام للمطور، مع تحليل تقارير نقاط البيع الأسبوعية.دليلالمطابقة Reconciliation: كشف البنك مقابل نظامككيف تقابل كل حركة في كشف البنك بقيد في دفترك على مراحل، ولماذا قد يتساوى الرصيدان وتحتهما دفع مكرر لا يكشفه إلا المرجع.دليلالمحفظة الرقمية من الداخل: دفتر القيد المزدوج Ledgerكيف تبني محفظة رقمية لا يضيع فيها ريال ولا يخصم مرتين، بالقيد المزدوج والمعاملات والمطابقة مع مزود الدفع.دليلالمصرفية المفتوحة Open Banking: إطار البنك المركزي والواجهاتما تتيحه المصرفية المفتوحة في المملكة وما تفرضه لائحتها، والموافقة من الطلب إلى الحذف، مع مختبر لتطبيق يقرأ بموافقة العميل فقط.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال وتشغيل المختبر يوم 27 أكتوبر 2026 على python:3.13-slim (Python 3.13.17) بمكتبات pyiso8583 4.0.1 وlxml 6.1.3 وsepaxml 2.7.0 (مخطط pain.001.001.09)، ببيانات اختبار فقط، ومراجعة صفحات البنك المركزي الأوروبي والاحتياطي الفيدرالي والبنك المركزي السعودي في التاريخ نفسه.
Could not load content
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.