اتخطّى للمحتوى

المرحلة 04 · الـ APIs والتكاملدرس 23 من 3417 دقيقة قراءةآخر تحديث:

Lesson 23 / 34

Payment Methodمن الصفر.

بناء طريقة دفع كاملة. بنية الـ gateway والـ commands، دورة حياة الدفع من التفويض للتحصيل، الواجهة في الـ checkout، والتعامل الآمن مع البيانات الحسّاسة.

● بناء Gateway Commands Webhooks أمان

طريقة الدفع في Magento 2 بتتبني بالـ Payment Gateway architecture: virtualType من Method\Adapter في di.xml مربوط بـ CommandPool فيه command لكل عملية authorize و capture و refund و void، وكل command بيتكوّن من Request Builder و Client و Validator و Response Handler. بيانات الكارت بتروح للبوابة مباشرة وإنت بتاخد توكن، والـ webhook لازم يتحقق من التوقيع ويحمي من التكرار.

// قبل ما تبدأ ده الجزء اللي فيه فلوس حقيقية. الخطأ هنا مش بطء ولا شكل وحش — خصم مضاعف، أو طلب اتدفع واتشحن من غير ما توصلك الفلوس. وفيه جزء تاني: أي كود بيلمس بيانات كروت بيدخلك في التزامات PCI قانونية. القاعدة اللي كل الدرس مبني عليها: متلمسش بيانات الكارت أبداً — خلّي البوابة تتعامل معاها وخُد توكن.
◆ Part 1 — الفهم
01

دورة حياة الدفع

المفاهيم اللي كل حاجة بعدها بتبني عليها.

1 Authorize التأكد إن الفلوس موجودة وحجزها — من غير خصم
2 Capture الخصم الفعلي — الفلوس بتتحوّل
3 Refund إرجاع كامل أو جزئي بعد التحصيل
أو قبل التحصيل ↓
4 Void / Cancel إلغاء التفويض — الحجز بيترفع
تشبيه زي حجز أوضة في فندق. الـ authorize هو لما ياخدوا بيانات كارتك ويتأكدوا إن فيه رصيد — بيحجزوا مبلغ بس مابيخدوهوش. الـ capture لما تسيب الفندق وتدفع فعلاً. لو لغيت الحجز قبل ما توصل، ده void — الحجز بيترفع ومفيش فلوس اتحركت أصلاً.
الاستراتيجيتان

Authorize فقط

تفويض وقت الطلب، وتحصيل عند الشحن. أأمن للتاجر.

Authorize + Capture

الاتنين مرة واحدة وقت الطلب. أبسط، ومناسب للمنتجات الرقمية.

i
اختار "Authorize فقط" لو بتبيع منتجات مادية. لو المنتج خلص من المخزن بعد الطلب، الـ void أسهل وأنضف من الـ refund — ومفيش رسوم على المعاملة في أغلب البوابات. والعميل مابيشوفش خصم ورجوع في كشف حسابه.
!
التفويض بينتهي. معظم البوابات بتلغيه تلقائياً بعد فترة (أسبوع لشهر حسب البوابة). لو الشحن بياخد وقت طويل، ممكن التفويض ينتهي قبل التحصيل — وتلاقي نفسك شحنت من غير ما تقبض. اعرف المدة من بوابتك واحسبها في عمليات الشحن.
• • •
02

أنماط التكامل

تلات طرق، وكل واحدة ليها تكلفة أمنية مختلفة.

pattern
Redirect
العميل بيروح لصفحة البوابة، بيدفع، وبيرجع.
الأأمنبياناته مابتمرّش على سيرفرك خالص. أقل التزامات PCI.
Hosted Fields / iFrame
حقول الكارت من البوابة معروضة جوّه صفحتك.
متوازنتجربة أحسن، والبيانات لسه بتروح للبوابة مباشرة.
Direct API
بتجمّع بيانات الكارت بنفسك وتبعتها للبوابة.
تجنّبهبيدخّلك في التزامات PCI كاملة. مايستخدمش إلا بضرورة قصوى وشهادة.
إزاي تختار

ابدأ بـ Hosted Fields لو البوابة بتدعمه — بيدي أحسن توازن بين التجربة والأمان. لو مش متاح، Redirect. والـ Direct API مايتفكّرش فيه إلا لو عندك شهادة PCI فعلاً وفاهم التزاماتها.

!
الفرق مش تقني بس — قانوني. لحظة ما بيانات كارت تعدّي على سيرفرك، بتدخل في نطاق PCI DSS: تدقيق سنوي، فحوصات أمنية، وقيود على السجلات والنسخ الاحتياطية. الـ redirect والـ hosted fields بيخلّوك برّه النطاق ده تقريباً — وده توفير ضخم في الوقت والتكلفة والمخاطرة.
• • •
03

بنية الـ Gateway

إزاي Magento بيفكّك عملية الدفع.

الفكرة

بدل كلاس واحد ضخم بيعمل كل حاجة، Magento بيفكّك العملية لـ قطع صغيرة كل واحدة ليها مسؤولية واحدة. ده اسمه Payment Gateway architecture.

Request
Transfer
Client
Validator
Handler
component
Command
عملية واحدة — authorize، capture، refund، void.
دورهبينظّم باقي المكوّنات لتنفيذ العملية.
Request Builder
بيحوّل بيانات الأوردر لشكل الطلب اللي البوابة بتفهمه.
دورههنا بتحدد إيه اللي بيتبعت.
Transfer Factory
بيغلّف الطلب مع الـ headers والـ URL.
دورهتجهيز الطلب للإرسال.
Client
بيبعت الطلب فعلاً ويستقبل الرد.
دورهالاتصال بالشبكة.
Validator
بيتأكد إن الرد سليم ونجح.
دورهلو فشل، بيرمي استثناء ويوقف العملية.
Response Handler
بياخد الرد ويحدّث بيانات الدفع في Magento.
دورهحفظ المرجع والحالة.
ليه التفكيك ده

عشان تقدر تغيّر جزء من غير ما تلمس الباقي. البوابة غيّرت شكل الطلب؟ تعدّل الـ Request Builder بس. ضفت حقل للرد؟ Handler جديد. وكل قطعة قابلة للاختبار لوحدها.

i
البنية دي بتبان زيادة عن اللزوم لأول وهلة — ملفات كتير لعملية بسيطة. بس على بوابة حقيقية بأربع عمليات وعشرات الحقول، بتبقى أنضف بكتير من كلاس واحد بألف سطر. الفايدة بتظهر في الصيانة مش في الكتابة الأولى.
• • •
◆ Part 2 — البناء
04

الإعداد الأساسي

تعريف الطريقة وإعداداتها.

etc/config.xml — القيم الافتراضيةxml
<config>
  <default>
    <payment>
      <mygateway>
        <!-- الكلاس اللي بيمثّل الطريقة -->
        <model>MyGatewayFacade</model>
        <active>0</active>
        <title>Pay by Card</title>

        <!-- authorize أو authorize_capture -->
        <payment_action>authorize</payment_action>

        <!-- الدول المسموحة -->
        <allowspecific>0</allowspecific>

        <!-- حدود المبلغ -->
        <min_order_total>0</min_order_total>
        <max_order_total>10000</max_order_total>

        <!-- العملات المدعومة -->
        <currency>SAR,AED,EGP</currency>
      </mygateway>
    </payment>
  </default>
</config>
شرح الإعدادات المهمة
  • payment_actionauthorize للتفويض بس، authorize_capture للاتنين.
  • min/max_order_total — حماية مهمة. بوابات كتير ليها حدود.
  • currency — لو البوابة بتدعم عملات محددة، حدّدها هنا.
  • active = 0دايماً معطّلة افتراضياً. التفعيل قرار واعي.
!
ابدأ بـ active = 0 دايماً. لو الموديول اتنصب على production والطريقة مفعّلة بإعدادات فاضية، العملاء هيشوفوها وهيحاولوا يدفعوا بيها وهتفشل — ودي تجربة سيئة جداً وممكن تخسرك طلبات.
etc/adminhtml/system.xml — شاشة الإعداداتxml
<group id="mygateway" translate="label"
  sortOrder="10" showInDefault="1" showInWebsite="1">
  <label>My Gateway</label>

  <field id="active" type="select" sortOrder="10"
    showInDefault="1" showInWebsite="1">
    <label>Enabled</label>
    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
  </field>

  <!-- المفتاح السري — لاحظ backend_model -->
  <field id="secret_key" type="obscure" sortOrder="30"
    showInDefault="1" showInWebsite="1">
    <label>Secret Key</label>
    <backend_model>
      Magento\Config\Model\Config\Backend\Encrypted
    </backend_model>
  </field>
</group>
!
المفاتيح السرية لازم: النوع obscure والـ backend_model يكون Encrypted. من غيرهم المفتاح بيتخزّن نص عادي في قاعدة البيانات — وأي حد عنده وصول للقراءة (نسخة احتياطية، تقرير، مطوّر مؤقت) هياخده.
• • •
05

الـ Commands

ربط العمليات بمكوّناتها.

etc/di.xml — الواجهة الرئيسيةxml
<!-- الكلاس اللي Magento بيتعامل معاه -->
<virtualType name="MyGatewayFacade"
  type="Magento\Payment\Model\Method\Adapter">
  <arguments>
    <argument name="code" xsi:type="const">
      Vendor\Payment\Model\Ui\ConfigProvider::CODE
    </argument>

    <!-- الواجهة في الـ checkout -->
    <argument name="formBlockType" xsi:type="string">
      Magento\Payment\Block\Form
    </argument>

    <!-- إيه اللي الطريقة دي تقدر تعمله -->
    <argument name="valueHandlerPool" xsi:type="object">
      MyGatewayValueHandlerPool
    </argument>

    <!-- العمليات المتاحة -->
    <argument name="commandPool" xsi:type="object">
      MyGatewayCommandPool
    </argument>
  </arguments>
</virtualType>
مجموعة العملياتxml
<virtualType name="MyGatewayCommandPool"
  type="Magento\Payment\Gateway\Command\CommandPool">
  <arguments>
    <argument name="commands" xsi:type="array">
      <item name="authorize" xsi:type="string">
        MyGatewayAuthorizeCommand
      </item>
      <item name="capture" xsi:type="string">
        MyGatewayCaptureCommand
      </item>
      <item name="refund" xsi:type="string">
        MyGatewayRefundCommand
      </item>
      <item name="void" xsi:type="string">
        MyGatewayVoidCommand
      </item>
    </argument>
  </arguments>
</virtualType>
عملية واحدة بمكوّناتهاxml
<virtualType name="MyGatewayAuthorizeCommand"
  type="Magento\Payment\Gateway\Command\GatewayCommand">
  <arguments>
    <!-- تجهيز الطلب -->
    <argument name="requestBuilder" xsi:type="object">
      MyGatewayAuthorizeRequest
    </argument>

    <!-- إرساله -->
    <argument name="transferFactory" xsi:type="object">
      Vendor\Payment\Gateway\Http\TransferFactory
    </argument>
    <argument name="client" xsi:type="object">
      Vendor\Payment\Gateway\Http\Client
    </argument>

    <!-- التحقق ومعالجة الرد -->
    <argument name="validator" xsi:type="object">
      Vendor\Payment\Gateway\Validator\ResponseValidator
    </argument>
    <argument name="handler" xsi:type="object">
      MyGatewayResponseHandlerComposite
    </argument>
  </arguments>
</virtualType>
i
لاحظ إن كل ده virtualTypes — يعني إعداد مش كود. Magento بيركّب الكلاسات من الأجزاء دي. الفايدة إنك بتكتب كود فعلي للأجزاء المميّزة بس (الـ request والـ client والـ handlers)، والباقي تركيب.
• • •
06

Request و Response

الكود الفعلي اللي هتكتبه.

Gateway/Request/AuthorizeBuilder.phpphp
namespace Vendor\Payment\Gateway\Request;

use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Payment\Gateway\Helper\SubjectReader;

class AuthorizeBuilder implements BuilderInterface
{
    public function build(array $buildSubject): array
    {
        // استخراج بيانات الدفع بالطريقة الصحيحة
        $paymentDO = SubjectReader::readPayment($buildSubject);
        $amount    = SubjectReader::readAmount($buildSubject);

        $payment = $paymentDO->getPayment();
        $order   = $paymentDO->getOrder();

        return [
            // التوكن من الواجهة — مش بيانات كارت
            'payment_token' => $payment->getAdditionalInformation(
                'payment_token'
            ),

            // المبلغ بالعملة الأساسية
            'amount'   => $this->formatAmount($amount),
            'currency' => $order->getCurrencyCode(),

            // مرجع فريد للمعاملة
            'reference' => $order->getOrderIncrementId(),

            'capture' => false,  // تفويض بس
        ];
    }

    // كتير من البوابات بتستقبل المبلغ بالوحدة الصغرى
    private function formatAmount(float $amount): int
    {
        return (int) round($amount * 100);
    }
}
!
تنسيق المبلغ مصدر أخطاء مكلّفة. بوابات كتير بتستقبل المبلغ بالوحدة الصغرى (قروش/هللات) كعدد صحيح. لو بعتّ 100.50 وهي متوقّعة 10050، هتخصم جزء من المبلغ أو مية ضعف. اقرا توثيق البوابة بعناية واختبر بمبالغ فيها كسور.
i
استخدم SubjectReader دايماً بدل ما تقرا المصفوفة مباشرة. بيتعامل مع الاختلافات بين نسخ Magento وبيرمي أخطاء واضحة لو البيانات ناقصة.
Gateway/Response/TxnIdHandler.phpphp
namespace Vendor\Payment\Gateway\Response;

use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Payment\Gateway\Helper\SubjectReader;

class TxnIdHandler implements HandlerInterface
{
    public function handle(
        array $handlingSubject,
        array $response
    ): void {
        $paymentDO = SubjectReader::readPayment(
            $handlingSubject
        );
        $payment = $paymentDO->getPayment();

        // مرجع المعاملة عند البوابة — مهم جداً
        $payment->setTransactionId($response['transaction_id']);

        // المعاملة مش مقفولة — لسه فيه capture
        $payment->setIsTransactionClosed(false);

        // نحتفظ بالمرجع للعمليات الجاية
        $payment->setAdditionalInformation(
            'gateway_reference',
            $response['reference']
        );
    }
}
الـ setIsTransactionClosed

بيقول لـ Magento هل المعاملة خلصت ولا لسه. في الـ authorize بتحطها false لأن لسه فيه capture جاي. في الـ capture النهائي بتبقى true.

!
لو حطّيتها true في الـ authorize بالغلط، Magento مش هيسمحلك تعمل capture بعدين — وهتلاقي أوردر مفوّض ومش قادر تحصّله من الأدمن.
التحقق من الردphp
class ResponseValidator extends AbstractValidator
{
    public function validate(array $validationSubject): ResultInterface
    {
        $response = SubjectReader::readResponse(
            $validationSubject
        );

        $isValid = isset($response['status'])
            && $response['status'] === 'approved';

        if ($isValid) {
            return $this->createResult(true);
        }

        // رسالة عامة للعميل — مش تفاصيل البوابة
        return $this->createResult(false, [
            __('Payment could not be processed.')
        ]);
    }
}
!
متعرضش رسالة البوابة الخام للعميل. ممكن تحتوي على تفاصيل داخلية أو أكواد بتساعد مهاجم يفهم النظام. سجّل التفاصيل في الـ log وأعرض رسالة عامة للعميل.
• • •
07

الواجهة في الـ Checkout

ربط الطريقة بواجهة العميل.

دوره

بيمرّر إعدادات الطريقة من PHP لواجهة الـ checkout (JavaScript).

Model/Ui/ConfigProvider.phpphp
namespace Vendor\Payment\Model\Ui;

use Magento\Checkout\Model\ConfigProviderInterface;

class ConfigProvider implements ConfigProviderInterface
{
    public const CODE = 'mygateway';

    public function __construct(
        private readonly Config $config
    ) {}

    public function getConfig(): array
    {
        return [
            'payment' => [
                self::CODE => [
                    // المفتاح العام فقط — مش السري
                    'publicKey' => $this->config->getPublicKey(),
                    'sandbox'   => $this->config->isSandbox(),
                ],
            ],
        ];
    }
}
!
خطأ خطير ومتكرّر: تمرير المفتاح السري هنا. أي حاجة في الـ ConfigProvider بتوصل لـ JavaScript في متصفح العميل — يعني أي حد يقدر يشوفها بفتح أدوات المطوّر. المفتاح العام بس.
التدفّق في الواجهة
  • العميل يدخل بيانات الكارت في حقول البوابة (مش حقولك).
  • مكتبة البوابة بتحوّل البيانات لـ توكن — البيانات راحت للبوابة مباشرة.
  • التوكن بيتحط في additionalData وبيتبعت لـ Magento.
  • الـ Request Builder بياخد التوكن ويبعته للبوابة مع الطلب.
i
النقطة 1 و2 هما اللي بيحموك. بيانات الكارت مابتلمسش سيرفرك ولا كودك — بتروح من متصفح العميل للبوابة مباشرة، وإنت بتاخد توكن مالوش قيمة لو اتسرّب.
• • •
◆ Part 3 — التشغيل
08

الـ Webhooks

لما البوابة تبلّغك بتغيير.

ليه محتاجها

عمليات كتير بتخلص بعدين: الدفع بتحويل بنكي، التحقق الإضافي، الاسترداد المتأخر. البوابة بتبعتلك إشعار لما الحالة تتغيّر.

Controller/Webhook/Index.phpphp
class Index extends Action implements CsrfAwareActionInterface
{
    // الـ webhook مالهوش form key
    public function validateForCsrf(RequestInterface $r): ?bool
    {
        return true;
    }

    public function createCsrfValidationException(
        RequestInterface $request
    ): ?InvalidRequestException {
        return null;
    }

    public function execute()
    {
        $payload   = $this->getRequest()->getContent();
        $signature = $this->getRequest()
            ->getHeader('X-Gateway-Signature');

        // 1. التحقق من التوقيع — إلزامي
        if (!$this->signatureValidator->isValid(
            $payload, $signature
        )) {
            $this->logger->warning('Invalid webhook signature');
            return $this->respond(401);
        }

        $data = json_decode($payload, true);

        // 2. الحماية من التكرار
        if ($this->log->isProcessed($data['event_id'])) {
            return $this->respond(200);
        }

        try {
            $this->processor->handle($data);
            $this->log->markProcessed($data['event_id']);
        } catch (\Exception $e) {
            $this->logger->error($e->getMessage());
            // 500 عشان البوابة تعيد المحاولة
            return $this->respond(500);
        }

        return $this->respond(200);
    }
}
التلات حمايات في الكود ده
  • التحقق من التوقيع — بيثبت إن الإشعار من البوابة فعلاً.
  • الحماية من التكرار — البوابات بتعيد الإرسال لو ماردتش بسرعة.
  • الرد بـ 500 عند الفشل — عشان البوابة تعيد المحاولة بدل ما الحدث يضيع.
!
التحقق من التوقيع إلزامي مش اختياري. من غيره، أي حد يعرف الرابط يقدر يبعت إشعار مزيّف يقول "الدفع نجح" — ويطلب منتجات مجاناً. الرابط مش سر، والبوابات بتبعت من عناوين معروفة.
i
نفس منطق الـ idempotency في درس RabbitMQ: افترض إن الرسالة هتوصل أكتر من مرة. البوابات بتعيد الإرسال لو الرد اتأخر، حتى لو إنت عالجت الحدث فعلاً.
• • •
09

الأمان

القواعد اللي مفيش تفاوض عليها.

rule
متخزّنش بيانات كروت
ولا رقم، ولا CVV، ولا تاريخ انتهاء — في أي مكان.
حتىفي الـ logs أو additional_information. خزّن التوكن بس.
المفاتيح السرية مشفّرة
backend_model = Encrypted.
ليهمن غيرها بتتخزّن نص عادي في قاعدة البيانات.
المفتاح العام بس في الواجهة
أي حاجة في ConfigProvider بتوصل للمتصفح.
ليهالعميل يقدر يشوف كل حاجة في JavaScript.
التحقق من توقيع الـ webhook
إلزامي في كل إشعار.
ليهمن غيره: إشعارات مزيّفة وطلبات مجانية.
المبلغ من السيرفر
من الأوردر، مش من أي حاجة جاية من العميل.
ليهوإلا العميل يدفع اللي هو عايزه.
تنظيف السجلات
امنع تسجيل الطلبات والردود الكاملة.
ليهممكن تحتوي على بيانات حسّاسة من غير ما تقصد.
إخفاء الحقول الحسّاسة في السجلاتxml
<!-- Magento بيوفّر آلية جاهزة -->
<virtualType name="MyGatewayLogger"
  type="Magento\Payment\Model\Method\Logger">
  <arguments>
    <argument name="debugReplaceKeys" xsi:type="array">
      <item name="0" xsi:type="string">card_number</item>
      <item name="1" xsi:type="string">cvv</item>
      <item name="2" xsi:type="string">secret_key</item>
    </argument>
  </arguments>
</virtualType>
!
سجلات الدفع بتتنسي وبتفضل سنين. لو سجّلت طلب فيه بيانات كارت مرة واحدة، البيانات دي بقت في ملفات السجل وفي كل نسخة احتياطية من بعدها. الإعداد ده بيمنعها من الأول — اعمله قبل أول اختبار مش بعدين.
• • •
10

الاختبار

إيه اللي لازم تجرّبه قبل الإطلاق.

المسار الناجح
  • طلب كامل بتفويض وتحصيل.
  • تحصيل جزئي (فاتورة على جزء من الطلب).
  • استرداد كامل وجزئي.
  • إلغاء قبل التحصيل (void).
مسارات الفشل — الأهم
  • كارت مرفوض — الرسالة واضحة والأوردر مااتعملش؟
  • رصيد غير كافٍ — نفس السؤال.
  • انقطاع الشبكة وقت الطلب — إيه اللي بيحصل؟
  • البوابة بتتأخر — فيه timeout؟ الأوردر بيتعمل ولا لأ؟
  • العميل يقفل المتصفح بعد الدفع — الأوردر بيكمّل بالـ webhook؟
  • webhook بتوقيع غلط — بيترفض؟
  • webhook مكرّر — بيتعالج مرة واحدة؟
!
السيناريو الخامس هو الأخطر: العميل دفع وقفل المتصفح قبل ما يرجع للموقع. من غير webhook سليم، الفلوس اتخصمت والأوردر مش موجود — والعميل هيتصل غاضب وإنت مالكش أثر للعملية. اختبره صراحةً.
قبل الإطلاق
  • كل السيناريوهات فوق على بيئة الاختبار للبوابة.
  • تأكد إن السجلات نضيفة من أي بيانات حسّاسة.
  • اختبر بمبالغ فيها كسور عشرية — للتأكد من التنسيق.
  • اختبر بكل العملات المدعومة.
  • معاملة حقيقية واحدة بمبلغ صغير على production، وتأكد من وصول الفلوس فعلاً في حساب البوابة.
i
الخطوة الأخيرة ضرورية. بيئة الاختبار بتتصرّف بشكل مختلف عن الحقيقية أحياناً — معاملة واحدة حقيقية بتكشف مشاكل الإعداد اللي مابتظهرش في الـ sandbox.
• • •
◆ Part 4 — الخلاصة
11

الفخاخ الشائعة

اقرا دي حتى لو مش هتقرا حاجة تانية.

  • تخزين بيانات كروت — التزامات قانونية وخطر تسريب. توكن بس.
  • المفتاح السري في ConfigProvider — بيوصل لمتصفح العميل.
  • webhook من غير تحقق توقيع — إشعارات مزيّفة وطلبات مجانية.
  • تنسيق مبلغ غلط — خصم جزء من المبلغ أو مية ضعف.
  • setIsTransactionClosed(true) في authorize — مش هتقدر تحصّل بعدين.
  • عرض رسالة البوابة الخام للعميل — كشف تفاصيل داخلية.
  • مفيش معالجة لـ webhook مكرّر — استرداد أو معالجة مضاعفة.
  • نسيان انتهاء التفويض — شحن من غير تحصيل.
  • الطريقة مفعّلة افتراضياً — عملاء بيحاولوا يدفعوا بطريقة مش متظبطة.
  • اختبار المسار الناجح بس — مسارات الفشل هي اللي بتوجع في الإنتاج.
i
لو معاملة فشلت، الترتيب: السجلات (بعد التأكد إنها منظّفة)لوحة البوابةحالة الأوردر والمعاملة في Magentoسجل الـ webhooks. لوحة البوابة بتقول الحقيقة عن الفلوس.
• • •
12

أسئلة الإنترفيو

الأسئلة المتكررة.

Q1إيه الفرق بين authorize و capture؟
الإجابة: الـ authorize بيتأكد إن الفلوس موجودة ويحجزها من غير خصم. الـ capture بيخصمها فعلاً. الفصل بينهم بيسمح بالتحصيل عند الشحن، وده أأمن للمنتجات المادية — لو المنتج خلص، الـ void أنضف من الـ refund. بس التفويض بينتهي بعد فترة، فده لازم يتحسب.
Q2إزاي تبني طريقة دفع في Magento؟
الإجابة: بالـ Gateway architecture — بفكّك العملية لـ commands، وكل command بيتكوّن من request builder و client و validator و response handler، وكلهم بيتربطوا بـ virtualTypes في di.xml. الفايدة إن كل جزء منفصل وقابل للاختبار والتغيير لوحده.
Q3إزاي تتعامل مع بيانات الكروت بأمان؟
الإجابة: مابتلمسهاش أصلاً. بستخدم hosted fields أو redirect، فالبيانات بتروح من متصفح العميل للبوابة مباشرة، وأنا باخد توكن. ده بيخرجني من نطاق PCI تقريباً. وبمنع تسجيل أي حقول حسّاسة بـ debugReplaceKeys.
Q4ليه التحقق من توقيع الـ webhook مهم؟
الإجابة: لأن رابط الـ webhook مش سري. من غير تحقق، أي حد يعرفه يقدر يبعت إشعار مزيّف يقول "الدفع نجح" ويطلب منتجات مجاناً. ولازم كمان حماية من التكرار لأن البوابات بتعيد الإرسال.
Q5العميل دفع وقفل المتصفح قبل ما يرجع — إيه اللي بيحصل؟
الإجابة: ده السيناريو اللي الـ webhook بيحله. الفلوس اتخصمت عند البوابة، فالبوابة بتبعت إشعار وأنا بكمّل الأوردر بناءً عليه. من غير webhook سليم، الفلوس اتخصمت والأوردر مش موجود — وده أسوأ سيناريو ممكن.
Q6إيه أخطر خطأ ممكن تعمله في تكامل دفع؟
الإجابة: اتنين متساويين: تخزين بيانات كروت (التزامات قانونية وخطر تسريب ضخم)، وwebhook من غير تحقق توقيع (طلبات مجانية). والتالت هو اختبار المسار الناجح بس — مسارات الفشل هي اللي بتوجع في الإنتاج.

Payment Method ✓

دلوقتي فاهم دورة حياة الدفع، أنماط التكامل وتكلفتها الأمنية، بنية الـ gateway ومكوّناتها، الـ webhooks، وإيه اللي لازم تختبره.

الخلاصة: متلمسش بيانات الكارت — خُد توكن، تحقّق من توقيع كل webhook، واحسب المبلغ من السيرفر دايماً.

// magento 2 · payment gateway · commands

خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.

كاتب الدرس

Abdulrahman Masoud

عندك سؤال على الدرس أو محتاج مساعدة في مشروع Magento؟ كلّمني.

أسئلة شائعة

إيه الفرق بين authorize و capture في Magento 2؟

الـ authorize بيتأكد إن الفلوس موجودة ويحجزها من غير خصم، والـ capture بيخصمها فعلًا. إعداد payment_action بيحدد الاستراتيجية: authorize بس والتحصيل عند الفاتورة أو الشحن، أو authorize_capture الاتنين وقت الطلب. للمنتجات المادية الأول أأمن لأن الـ void أنضف من الـ refund لو المنتج خلص، بس التفويض بينتهي بعد فترة فلازم تحسبها.

إزاي أبني طريقة دفع مخصّصة في Magento 2؟

بالـ Payment Gateway architecture: virtualType من Magento\Payment\Model\Method\Adapter في di.xml بيتربط بـ CommandPool فيه command لكل عملية، وكل GatewayCommand بيتكوّن من Request Builder و Transfer Factory و Client و Validator و Response Handler. القيم الافتراضية في etc/config.xml تحت payment والشاشة في system.xml، وابدأ بـ active = 0 دايمًا.

إزاي أتعامل مع بيانات الكروت بأمان في Magento 2؟

مابتلمسهاش أصلًا. استخدم Hosted Fields أو Redirect فالبيانات بتروح من متصفح العميل للبوابة مباشرة وإنت بتاخد توكن في additionalData، وده بيخرجك من نطاق PCI DSS تقريبًا. متخزّنش رقم ولا CVV في أي مكان حتى الـ logs، خزّن المفاتيح السرية بحقل obscure و backend_model Encrypted، وامنع تسجيل الحقول الحسّاسة بـ debugReplaceKeys في الـ Logger.

ليه التحقق من توقيع الـ webhook إلزامي في بوابة الدفع؟

لأن رابط الـ webhook مش سر. من غير تحقق أي حد يعرفه يقدر يبعت إشعار مزيّف يقول الدفع نجح ويطلب منتجات مجانًا. الـ controller لازم يطبّق CsrfAwareActionInterface لأنه مالوش form key، يتحقق من التوقيع ويرجّع 401 لو غلط، يحمي من التكرار بـ event_id لأن البوابات بتعيد الإرسال، ويرجّع 500 عند الفشل عشان البوابة تعيد المحاولة.

إيه اللي بيحصل لو العميل دفع وقفل المتصفح قبل ما يرجع للموقع؟

ده السيناريو اللي الـ webhook بيحله. الفلوس اتخصمت عند البوابة، فالبوابة بتبعت إشعار بتغيّر الحالة وإنت بتكمّل الأوردر بناءً عليه. من غير webhook سليم الفلوس اتخصمت والأوردر مش موجود، وده أسوأ سيناريو ممكن في أي تكامل دفع. اختبره صراحةً على sandbox البوابة قبل الإطلاق مع باقي مسارات الفشل.

ليه مش قادر أعمل capture من الأدمن بعد الـ authorize؟

غالبًا الـ Response Handler حط setIsTransactionClosed(true) في الـ authorize. القيمة دي بتقول لـ Magento إن المعاملة خلصت، فمش هيسمح بالتحصيل بعدها. في الـ authorize خليها false لأن لسه فيه capture جاي، وفي الـ capture النهائي true. وخزّن setTransactionId بمرجع البوابة لأن الرفاند والـ void محتاجينه.

ليه المبلغ بيوصل للبوابة غلط في تكامل الدفع؟

غالبًا التنسيق. بوابات كتير بتستقبل المبلغ بالوحدة الصغرى كعدد صحيح، فلو بعتّ 100.50 وهي متوقّعة 10050 هتخصم جزء من المبلغ أو مية ضعف. اقرا المبلغ من SubjectReader::readAmount في الـ Request Builder، واحسبه من الأوردر على السيرفر مش من أي قيمة جاية من العميل، واختبر بمبالغ فيها كسور وبكل العملات المدعومة.