روزنوشت

می نویسم؛ تا به یاد آورم روزی، بودم

روزنوشت

می نویسم؛ تا به یاد آورم روزی، بودم

روزنوشت

من آرزویی ندارم، من از چیزی نمی‌ترسم، من آزادم...

«کازانتزاکیس»

دنبال کنندگان ۳ نفر
این وبلاگ را دنبال کنید
آخرین نظرات

کتابخانه مدیریت درگاه های بانکی برای PHP

سه شنبه, ۱۹ مرداد ۱۳۹۵، ۰۳:۰۶ ق.ظ

کتابخانه ای که برای مدیریت درگاه بانکی در شرکت ازش استفاده می کردیم، مشکلاتی داشت و خیلی ازش راضی نبودیم.

یکی از مشکلات عمده ی این کتابخانه این بود که خیلی استاندارد نوشته نشده بود (و البته قدیمی بود) که اضافه کردن درگاه های دیگه رو دشوار می کرد.

 

از همون ابتدای بازنویسی سیستم ارائه ی محصول شرکت قصد داشتم که این ماژول رو تغییر بدم، اما بخاطر حجم کارهایی که باید انجام می شد، این موضوع به حاشیه رفت، چون به هرحال کار میکرد.

 

این اواخر متوجه شدم که این کتابخانه که برای مدیریت درگاه "به پرداخت ملت" ازش استفاده میکردیم مشکلاتی داره، به همین دلیل تلاش کردم که یک کتابخانه ی جایگزین پیدا کنم. اما متاسفانه جستجوی من نتیجه ای نداشت. من به کتابخانه ای نیاز داشتم که استاندارد نوشته شده باشه و از درگاه های متفاوتی پشتیبانی کنه، یا حداقل سازوکارش به گونه ای باشه که بشه راحت تر درگاه های جدید رو بهش اضافه کرد.

 

به دلیل اینکه نتونستم کتابخانه ی جایگزینی پیدا کنم (شاید وجود داشته باشه، و منطقاً می بایست وجود داشته باشه !)، دست به کار شدم و خودم شروع به نوشتنش کردم.

 

ایده ی من این بود که این کتابخانه باید کارکرد ساده ای داشته باشه و تا حد ممکن ماژولار طراحی بشه تا استفاده از درگاه های دیگه رو راحت کنه و بشه به راحتی درگاه های جدید رو بهش اضافه کرد (بیشتر از همه، خودم از این متنفرم که هربار برای اضافه کردن درگاه جدید، کل کدهام رو تغییر بدم).

 

نتیجه، کتابخانه ی مدیریت درگاه های بانکی ایران هست که در گیت هاب در دسترس هست:

روش صحیح استفاده از کتابخانه، نصب از طریق Composer هست:

composer require nevercom/iripg

 

برای استفاده از این کتابخانه، ابتدا باید جداول موردنیاز در دیتابیس ایجاد بشه. در این کتابخانه بصورت پیشفرض کلاسی تحت عنوان IPGDatabase وجود داره که در اون ساختار جداول (DB Schema) تعریف شده و از دیتابیس MySQL استفاده می کنه.

گرچه ساختار این کتابخانه به گونه ای هست که میتونید کلاس خودتون رو برای مدیریت دیتابیس بنویسید، که در اون انتخاب سیستم دیتابیس و ساختار جداول دست خودتون هست. تنها نیاز این هست که این کلاس می بایست از کلاس AbstractIPGDatabaseManager مشتق (extend) شده باشه و متدهای اون کلاس رو پیاده سازی کرده باشه.

 

در صورتی که از کلاس پیشفرض استفاده کنید، باید جداول موردنیاز رو ایجاد کنید:

-- phpMyAdmin SQL Dump
-- version 4.5.0.2
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Generation Time: Aug 07, 2016 at 10:16 AM
-- Server version: 5.5.50-0ubuntu0.14.04.1-log
-- PHP Version: 5.5.9-1ubuntu4.19

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";


-- --------------------------------------------------------

--
-- Table structure for table `bank_transactions`
--

CREATE TABLE `bank_transactions` (
  `pay_id` bigint(20) NOT NULL,
  `transaction_id` bigint(20) NOT NULL,
  `bank_name` varchar(255) NOT NULL,
  `bank_id` tinyint(4) NOT NULL,
  `amount` bigint(20) NOT NULL,
  `reference_id` varchar(256) NOT NULL,
  `status` tinyint(3) NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `bank_transactions`
--
ALTER TABLE `bank_transactions`
  ADD PRIMARY KEY (`pay_id`),
  ADD KEY `transaction_id` (`transaction_id`),
  ADD KEY `reference_id` (`reference_id`(255)),
  ADD KEY `status` (`status`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `bank_transactions`
--
ALTER TABLE `bank_transactions`
  MODIFY `pay_id` bigint(20) NOT NULL AUTO_INCREMENT;

-- --------------------------------------------------------

--
-- Table structure for table `bank_logs`
--

CREATE TABLE `bank_logs` (
  `id` int(20) NOT NULL,
  `pay_id` int(20) NOT NULL,
  `method` varchar(2048) NOT NULL,
  `status_code` int(11) NOT NULL,
  `input` text NOT NULL,
  `output` text NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `bank_logs`
--
ALTER TABLE `bank_logs`
  ADD PRIMARY KEY (`id`),
  ADD KEY `pay_id` (`pay_id`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `bank_logs`
--
ALTER TABLE `bank_logs`
  MODIFY `id` int(20) NOT NULL AUTO_INCREMENT;
 

بعد از اینکه دیتابیس رو آماده کردید، باید درگاه های بانکی که میخواید ازش استفاده کنید رو آماده کنید.

در زمانی که این پست نوشته میشه، دو درگاه "به پرداخت ملت" و "پارسیان" به کتابخانه اضافه شدن. قصد دارم که درگاه های بیشتری رو اضافه کنم، و البته بخاطر ساختار کتابخانه، اضافه کردن درگاه جدید پیچیدگی چندانی نداره.

 

فرض می کنیم می خواید از درگاه به پرداخت ملت استفاده کنید، برای این منظور باید کلاسی بسازید که از کلاس MellatIPG مشتق شده باشه. دلیل این کار این هست که هر درگاه علاوه بر IP پذیرنده، یکسری شناسه های امنیتی هم برای احراز هویت نیاز داره که باید برای کلاس تعریف بشن و از طرف دیگه ممکنه یک پذیرنده نیاز داشته باشه از چند اکانت متفاوت از یک درگاه در اپلیکیشنش استفاده کنه.

 

<?php

namespace Test;

use IPG\Gateways\Mellat\MellatIPG;

class MyMellat extends MellatIPG {
    protected $terminalId   = '123456789';
    protected $userName     = 'username';
    protected $userPassword = 'P4$$w0rD';
}

برای سایر درگاه ها هم به همین شکل کلاس پایه باید extend بشه و فیلدهای protected اون کلاس که برای احراز هویت هست، مقداردهی بشه

 

بعد از آماده کردن پیش نیازها، نوبت به استفاده از کتابخانه برای پرداخت میرسه.

کلاسی که مدیریت پرداخت رو به عهده داره، کلاس IPGManager هست.

$man = new IPGManager(new MyMellat(), new IPGDatabase('username', 'password', 'db_name'));

همونطور که میبینید، Constructor این کلاس، دو پارامتر میگیره که اولی کلاس مربوط به درگاهی هست که باید پرداخت از اون مسیر صورت بگیره و دوم کلاس مدیریت دیتابیس هست.

 

پس از راه‌اندازی کلاس، باید متد startPayment برای شروع پرداخت صدا زده بشه:

$res = $man->startPayment(time(), 10000, 'http://your-site.ir/callback.php');

پارامتر اول، کد تراکنش یکتایی هست که برای اون محصول تولید کردید. این کد تراکنش در انتهای پرداخت در اختیارتون قرار میگیره تا با استفاده از اون تشخیص بدید که این عملیات پرداخت، برای کدوم محصول بوده و درصورت موفق بودن اقدام به تحویل محصول کنید.

پارامتر دوم مبلغ به ریال و پارامتر سوم آدرس صفحه ی Callback هست که پس از انجام پرداخت توسط مشتری، بانک اطلاعات رو به اون صفحه ارسال می کنه و در اون صفحه باید تایید صحت پرداخت صورت بگیره.

 

خروجی این متد، شئ‌ی از جنس PaymentResponse هست که شامل اطلاعات زیر هست

  • isSuccessful: مشخص می کنه که عملیات با موفقیت انجام شده یا خیر
  • transactionId: کد تراکنش
  • referenceId: شماره ارجاع اولیه ی بانک
  • targetUrl: آدرس URLی که باید داده ها براش ارسال بشن و درواقع مشتری رو به اون صفحه هدایت کنیم
  • data: داده هایی که باید به targetUrl ارسال بشن.

بعنوان مثال این کد درصورت موفق بودن مرحله ی ابتدایی پرداخت، مشتری رو به صفحه ی پرداخت هدایت می کنه:

if (!$res->isIsSuccessful()) {
    echo 'FAILED';
    exit();
}
$location = 'poster.php?data=' . json_encode($res->getData()) . '&url=' . $res->getTargetUrl();
header("Location: {$location}");

این کد اطلاعات رو به یک صفحه ی واسط میفرسته که کارش اینه که data رو به URL موردنظر POST کنه، شما از هر روشی که دوست داشتید برای این منظور استفاده کنید.

برای نمونه، سورس فایل poster.php رو قرار میدم:

<html>
<head>
    <script>
        function getParameterByName(name) {
            name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
            var regex   = new RegExp("[\\?&]" + name + "=([^&#]*)"),
                results = regex.exec(location.search);
            return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
        }

        var jsondata = getParameterByName("data");
        var obj = JSON.parse(jsondata);
        var form = document.createElement("form");
        form.setAttribute("method", "POST");
        form.setAttribute("target", "_self");
        form.setAttribute("action", getParameterByName("url"));

        var len = count = Object.keys(obj).length;

        for (var key in obj) {
            var hiddenField1 = document.createElement("input");
            hiddenField1.setAttribute("name", key);
            hiddenField1.setAttribute("value", obj[key]);

            form.appendChild(hiddenField1);

        }

        form.submit();
        document.body.removeChild(form);

    </script>

</head>
<body></body>
</html>

 

تا اینجای کار مشتری رو به صفحه ی پرداخت هدایت کردیم و باید منتظر بمونیم تا درگاه بانک، اطلاعات پرداخت رو به صفحه ی Callback ما ارسال کنه.

 

ساختار صفحه ی Callback به این شکل هست.

 

در ابتدا باید کلاس IPGManager رو راه‌اندازی کنیم، اما چون در این مرحله نمیدونیم که کدوم درگاه بانکی اطلاعات رو برامون ارسال کرده، از یک متد کمکی برای راه‌اندازی کلاس استفاده می کنیم.

$man = IPGManager::fromCallback($_REQUEST, new IPGDatabase('user', 'pass', 'db'));

پارامتر اول، متغیر $_REQUEST هست که حاوی مقادیر POST و GET ارسالی به سمت این صفحه هست و پارامتر دوم هم کلاس مدیریت دیتابیس هست.

 

پس از اون، با متد validatePayment چک می کنیم که پرداخت موفق بوده یا خیر.

خروجی این متد، آبجکتی از جنس ValidationResponse هست. این کلاس این اطلاعات رو در اختیارتون میزاره:

  • isValid: آیا پرداخت موفق بوده یا خیر
  • referenceId: کد ارجاع نهایی بانک که برای پیگیری های بعدی ازش استفاده میشه
  • payId: کد تراکنش پرداخت که برای پیگیری های بعدی ازش استفاده میشه، در واقع این یک شناسه ی منحصر بفرد هست که در دیتابیس برای هر تراکنش پرداخت تولید میشه و متفاوت از کد تراکنشی هست که شما در مرحله اول وارد کردید
  • transactionId: کد تراکنشی هست که شما تعیین کردید و برای ارتباط بین پرداخت انجام شده و سفارش محصول در دیتابیستون ازش استفاده می کنید.

در این مرحله اگر مقدار isValid برابر TRUE بود، یعنی پرداخت به درستی انجام شده و می بایست اقدام به تحویل محصول کنید. (توجه کنید که این متد در دو مرحله، ابتدا اطلاعات ارسالی به صفحه رو بررسی می کنه و درصورت تایید، اقدام به صدا زدن متد verify میکنه، که از بانک استعلام صحت پرداخت رو میگیره و همچنین باعث میشه وجه از حساب مشتری، به حساب پذیرنده منتقل بشه که یک مرحله ی حیاتی هست و تا انجام نشه پولی به حساب پذیرنده منتقل نمیشه. بنابراین نیاز نیست پس از این مرحله، verify صورت بگیره)

 

درصورتی که موفق به ارائه ی محصول نشدید، با متد reversal می تونید پول رو به حساب مشتری برگشت بدید.

همچنین بصورت اختیاری میتونید متد settle رو پس از تحویل محصول صدا بزنید. این متد کار تسویه حساب رو انجام میده و برخی درگاه ها ازش پشتیبانی میکنن. گرچه اگر این متد صدا نشه، به دلیل اینکه ما verify رو انجام دادیم، تسویه حساب بصورت خودکار انجام میشه پس صدا زدن این متد اختیاری هست.

 

کد کامل صفحه ی Callback رو ادامه میتونید ببینید:

<?php

require_once 'vendor/autoload.php';
use IPG\IPGDatabase;
use IPG\IPGManager;

$man = IPGManager::fromCallback($_REQUEST, new IPGDatabase('user', 'pass', 'db'));

$v = $man->validatePayment($_REQUEST);
if ($v->isValid()) {
    // You should deliver the product now

    // transactionId is the unique id that you provided in the startPayment method
    // and is used to associate a payment transaction to a product order in your database
    // you can use this id to find which order this payment is for, and deliver that product
    $transactionId = $v->getTransactionId();

    if (productDeliverIsSuccessful($transactionId)) {
        // Optionally you can settle the payment, if you dont it will be settled automatically
        $man->settle($v->getPayId(), $v->getReferenceId());
    } else {
        // if any error occured upon delivering the product
        // you can reverse the payment, which will return the money to the customer
        $man->reversal($v->getPayId(), $v->getReferenceId());
    }

} else {
    // you can get the last error after calling each method
    $errorCode    = $man->getErrorCode();
    $errorMessage = $man->getErrorMessage();
}

// render the result page

همچنین اگر در هر مرحله خطایی رخ داد یا پیغام ناموفق بودن دریافت کردید، با متد های getErrorCode و getErrorMessage میتونید شرح خطا رو دریافت کنید.

 

این کتابخانه بصورت پیشفرض لاگ متدها به همراه ورودی و خروجی متد ها رو دخیره می کنه که برای بررسی فرآیند یک تراکنش می تونه مفید باشه. در صورتی که نیازی به این قابلیت ندارید، میتونید با استفاده از متد setLoggingEnabled  این قابلیت رو غیرفعال کنید:

$man->setLoggingEnabled(false);

 

برای اضافه کردن درگاه های بیشتر، کلاس جدیدی ایجاد کنید که باید extend شده از کلاس AbstractIPG باشه.

 

پس از پیاده سازی منطق مربوط به اون درگاه در هر متد، کلاس جدید آماده ی استفاده در این کتابخانه هست.

 

امیدوارم که این کتابخانه برای برنامه نویسان مفید واقع بشه و تقاضا دارم اگر باگی در این کتابخانه پیدا کردید بهم اطلاع بدید و یا بهتر از اون، در گیت هاب برای تغییرات مدنظرتون Pull Request بدید.

 

پیروز باشید

نظرات (۰)

هیچ نظری هنوز ثبت نشده است
ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی