diff --git a/.gitignore b/.gitignore index 8e830ab..de43626 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,11 @@ Temporary Items # VSCode project's cache .vscode +# phpunit cache +.phpunit.result.cache + # Vendor directory vendor/ + +# Composer.lock file +composer.lock diff --git a/.travis.yml b/.travis.yml index bad31a9..9305f90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,13 @@ language: php + php: - - '7.2' - - '7.3' -before_script: composer install + - 7.2 + - 7.3 + - 7.4 + +before_script: + - travis_retry composer self-update + - travis_retry composer update + - travis_retry composer install --prefer-source --no-interaction --dev + +script: phpunit diff --git a/README-FA.md b/README-FA.md index 1ef221d..8e28c76 100644 --- a/README-FA.md +++ b/README-FA.md @@ -8,8 +8,8 @@ [![Software License][ico-license]](LICENSE.md) [![Latest Version on Packagist][ico-version]][link-packagist] [![Total Downloads on Packagist][ico-download]][link-packagist] -[![StyleCI](https://github.styleci.io/repos/169948762/shield?branch=master)](https://github.styleci.io/repos/169948762) -[![Maintainability](https://api.codeclimate.com/v1/badges/e6a80b17298cb4fcb56d/maintainability)](https://codeclimate.com/github/shetabit/multipay/maintainability) +[![StyleCI](https://github.styleci.io/repos/268039684/shield?branch=master)](https://github.styleci.io/repos/268039684) +[![Maintainability](https://api.codeclimate.com/v1/badges/3aa790c544c9f2132b16/maintainability)](https://codeclimate.com/github/shetabit/multipay/maintainability) [![Quality Score][ico-code-quality]][link-code-quality] این پکیج برای پرداخت آنلاین توسط درگاه‌های مختلف در پی اچ پی ایجاد شده است. @@ -27,6 +27,7 @@ [به منظور کمک مالی کلیک کنید](https://zarinp.al/@mahdikhanzadi) :sunglasses: :bowtie: +درصورتی که از Laravel استفاده میکنید میتونید از پکیج [shetabit/payment](https://github.com/shetabit/payment) استفاده کنید. # لیست محتوا @@ -34,12 +35,13 @@ - [نصب](#نصب) - [تنظیمات](#تنظیمات) - [طریقه استفاده](#طریقه-استفاده) - - [کار با صورتحساب ها](#کار-با-صورتحساب-ها) - - [ثبت درخواست برای پرداخت صورتحساب](#ثبت-درخواست-برای-پرداخت-صورتحساب) - - [پرداخت صورتحساب](#پرداخت-صورتحساب) - - [اعتبار سنجی پرداخت](#اعتبار-سنجی-پرداخت) - - [ایجاد درایور دلخواه](#ایجاد-درایور-دلخواه) - - [متدهای سودمند](#متدهای-سودمند) + - [کار با صورتحساب ها](#کار-با-صورتحساب-ها) + - [ثبت درخواست برای پرداخت صورتحساب](#ثبت-درخواست-برای-پرداخت-صورتحساب) + - [پرداخت صورتحساب](#پرداخت-صورتحساب) + - [اعتبار سنجی پرداخت](#اعتبار-سنجی-پرداخت) + - [ایجاد درایور دلخواه](#ایجاد-درایور-دلخواه) + - [متدهای سودمند](#متدهای-سودمند) +- [درایور آفلاین (برای تست)](#درایور-آفلاین) - [تغییرات](#تغییرات) - [مشارکت کننده ها](#مشارکت-کننده-ها) - [امنیت](#امنیت) @@ -48,22 +50,44 @@ # درایورهای موجود -- [اسان پرداخت](https://asanpardakht.ir/) :warning: (تست نشده) -- [به‌پرداخت (mellat)](http://www.behpardakht.com/) :heavy_check_mark: -- [ایدی پی](https://idpay.ir/) :heavy_check_mark: -- [ایرانکیش](http://irankish.com/) :heavy_check_mark: -- [نکست پی](https://nextpay.ir/) :heavy_check_mark: +- [آقای پرداخت](https://aqayepardakht.ir/) :heavy_check_mark: +- [آسان‌پرداخت](https://asanpardakht.ir/) :heavy_check_mark: +- [آتی‌پی](https://www.atipay.net/) :heavy_check_mark: +- [ازکی‌وام (پرداخت اقساطی)](https://www.azkivam.com/) :heavy_check_mark: +- [به‌پرداخت (ملت)](http://www.behpardakht.com/) :heavy_check_mark: +- [بیت‌پی](https://bitpay.ir/) :heavy_check_mark: +- [دیجی‌پی](https://www.mydigipay.com/) :heavy_check_mark: +- [اعتبارینو (پرداخت اقساطی)](https://etebarino.com/) :heavy_check_mark: +- [فن‌آوا‌کارت](https://www.fanava.com/) :heavy_check_mark: +- [گویاپـــی](https://gooyapay.ir/) :heavy_check_mark: +- [آی‌دی‌پی](https://idpay.ir/) :heavy_check_mark: +- [ایران‌کیش](http://irankish.com/) :heavy_check_mark: +- [جیبیت](https://jibit.ir/) :heavy_check_mark: +- [لوکال](#local-driver) :heavy_check_mark: +- [مینی پی](https://minipay.me/) :heavy_check_mark: +- [نکست‌پی](https://nextpay.ir/) :heavy_check_mark: +- [امیدپی](https://sayancard.ir/) :heavy_check_mark: - [پارسیان](https://www.pec.ir/) :heavy_check_mark: -- [پاسارگاد](https://www.bpi.ir/) :heavy_check_mark: -- [پی آی ار](https://pay.ir/) :heavy_check_mark: -- [پی پال](http://www.paypal.com/) (به زودی در ورژن بعدی اظافه میشود) -- [پی پینگ](https://www.payping.ir/) :heavy_check_mark: -- [پی استار](http://paystar.ir/) :heavy_check_mark: +- [پاسارگاد](https://bpi.ir/) :heavy_check_mark: +- [پی‌فا](https://payfa.com/) :heavy_check_mark: +- [پی‌آی‌آر](https://pay.ir/) :heavy_check_mark: +- [پی‌پال](http://www.paypal.com/) (به زودی در ورژن بعدی اضافه می‌شود) +- [پی‌پینگ](https://www.payping.ir/) :heavy_check_mark: +- [پی‌استار](http://paystar.ir/) :heavy_check_mark: - [پولام](https://poolam.ir/) :heavy_check_mark: -- [سداد (بانک ملی)](https://sadadpsp.ir/) :heavy_check_mark: +- [رایان‌پی](https://rayanpay.com/) :heavy_check_mark: +- [سداد (ملی)](https://sadadpsp.ir/) :heavy_check_mark: - [سامان](https://www.sep.ir) :heavy_check_mark: -- [یک پی](https://yekpay.com/) :heavy_check_mark: -- [زرین پال](https://www.zarinpal.com/) :heavy_check_mark: +- [سپ (درگاه الکترونیک سامان) کشاورزی و صادرات](https://www.sep.ir) :heavy_check_mark: +- [سپهر (صادرات)](https://www.sepehrpay.com/) :heavy_check_mark: +- [سپرده](https://sepordeh.com/) :heavy_check_mark: +- [سیزپی](https://www.sizpay.ir/) :heavy_check_mark: +- [اسنپ‌پی](https://snapppay.ir/) :heavy_check_mark: +- [تومن](https://tomanpay.net/) :heavy_check_mark: +- [وندار](https://vandar.io/) :heavy_check_mark: +- [والتا](https://walleta.ir/) :heavy_check_mark: +- [یک‌پی](https://yekpay.com/) :heavy_check_mark: +- [زرین‌پال](https://www.zarinpal.com/) :heavy_check_mark: - [زیبال](https://www.zibal.ir/) :heavy_check_mark: - درایورهای دیگر ساخته خواهند شد یا اینکه بسازید و درخواست `merge` بدید. @@ -84,9 +108,7 @@ $ composer require shetabit/multipay ## تنظیمات -a. Copy `config/payment.php` and put it somewhere in your project. (you can also find it in `vendor/shetabit/multipay/config/payment.php` path). - -a. ابتدا فایل حاوی تنظیمات را از مسیر `config/payment.php` به درون پروژه خود کپی کنید. (فایل تنظیمات رو میتونید از مسیر `vendor/shetabit/multipay/config/payment.php نیز پیدا کرده و کپی کنید) +a. ابتدا فایل حاوی تنظیمات را از مسیر `config/payment.php` به درون پروژه خود کپی کنید. (فایل تنظیمات رو میتونید از مسیر `vendor/shetabit/multipay/config/payment.php` نیز پیدا کرده و کپی کنید) b. درون فایل تنظیمات در قسمت `default driver` می‌توانید درایوری که قصد استفاده از ان را دارید قرار دهید تا تمامی پرداخت ها از آن طریق انجام شود. @@ -126,7 +148,7 @@ c. سپس از روی کلاس `Payment` یک نمونه ایجاد کنید و ```php - use Shetabit\MultipayMultipay; + use Shetabit\Multipay\Payment; // load the config file from your project $paymentConfig = require('path/to/payment.php'); @@ -196,24 +218,30 @@ $invoice->detail('detailName1','your detail1 goes here') ```php // At the top of the file. use Shetabit\Multipay\Invoice; -use Shetabit\Multipay\Facade\Payment; +use Shetabit\Multipay\Payment; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase the given invoice. -Payment::purchase($invoice,function($driver, $transactionId) { +$payment->purchase($invoice,function($driver, $transactionId) { // We can store $transactionId in database. }); // Purchase method accepts a callback function. -Payment::purchase($invoice, function($driver, $transactionId) { +$payment->purchase($invoice, function($driver, $transactionId) { // We can store $transactionId in database. }); // You can specify callbackUrl -Payment::callbackUrl('http://yoursite.com/verify')->purchase( +$payment->callbackUrl('http://yoursite.com/verify')->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. @@ -232,19 +260,25 @@ Payment::callbackUrl('http://yoursite.com/verify')->purchase( ```php // At the top of the file. use Shetabit\Multipay\Invoice; -use Shetabit\Multipay\Facade\Payment; +use Shetabit\Multipay\Payment; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase and pay the given invoice. // You should use return statement to redirect user to the bank page. -return Payment::purchase($invoice, function($driver, $transactionId) { +return $payment->purchase($invoice, function($driver, $transactionId) { // Store transactionId in database as we need it to verify payment in the future. })->pay(); // Do all things together in a single line. -return Payment::purchase( +return $payment->purchase( (new Invoice)->amount(1000), function($driver, $transactionId) { // Store transactionId in database. @@ -264,10 +298,16 @@ return Payment::purchase( ```php // At the top of the file. -use Shetabit\Multipay\Facade\Payment; +use Shetabit\Multipay\Payment; use Shetabit\Multipay\Exceptions\InvalidPaymentException; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // You need to verify the payment to ensure the invoice has been paid successfully. // We use transaction id to verify payments // It is a good practice to add invoice amount as well. @@ -295,7 +335,7 @@ try { #### ایجاد درایور دلخواه: - برای ایجاد درایور جدید ابتدا نام (اسم) درایوری که قراره بسازید رو به لیست درایور ها اضافه کنید و لیست تنظیات مورد نیاز را نیز مشخص کنید. +برای ایجاد درایور جدید ابتدا نام (اسم) درایوری که قراره بسازید رو به لیست درایور ها اضافه کنید و لیست تنظیات مورد نیاز را نیز مشخص کنید. @@ -411,14 +451,20 @@ class MyDriver extends Driver ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\Facade\Payment; + use Shetabit\Multipay\Payment; ... + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase the given invoice. - Payment::callbackUrl($url)->purchase( + $payment->callbackUrl($url)->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. @@ -435,11 +481,17 @@ class MyDriver extends Driver ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\Facade\Payment; + use Shetabit\Multipay\Payment; ... + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Purchase (we set invoice to null). - Payment::callbackUrl($url)->amount(1000)->purchase( + $payment->callbackUrl($url)->amount(1000)->purchase( null, function($driver, $transactionId) { // We can store $transactionId in database. @@ -456,14 +508,20 @@ class MyDriver extends Driver ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\Facade\Payment; + use Shetabit\Multipay\Payment; ... + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase the given invoice. - Payment::via('driverName')->purchase( + $payment->via('driverName')->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. @@ -473,8 +531,74 @@ class MyDriver extends Driver
+- ###### `config`: به منظور تغییر تنظیمات در هنگام اجرای برنامه مورد استفاده قرار میگیرد + +
+ + ```php + // At the top of the file. + use Shetabit\Multipay\Invoice; + use Shetabit\Multipay\Payment; + ... + + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + -#### رویدادها + // Create new invoice. + $invoice = (new Invoice)->amount(1000); + + // Purchase the given invoice with custom driver configs. + $payment->config('mechandId', 'your mechand id')->purchase( + $invoice, + function($driver, $transactionId) { + // We can store $transactionId in database. + } + ); + + // Also we can change multiple configs at the same time. + $payment->config(['key1' => 'value1', 'key2' => 'value2'])->purchase( + $invoice, + function($driver, $transactionId) { + // We can store $transactionId in database. + } + ); + ``` + +
+ +- ###### `فیلدهای اضافی (دلخواه)`: در نظر داشته باشید که تمامی درگاه‌ها از این امکان پشتیبانی نمیکنند. +درگاه **پرداخت الکترونیک سامان** تا ۴ فیلد اضافه را پشتبانی میکند و هرکدام از فیلدها تا ۵۰ کاراکتر اطلاعات را میتوانند در خود نگهداری کنند. + +اطلاعات این فیلدها در هنگام گزارش گیری در پنل پذیرنده نمایش داده میشوند. + +شما میتوانید اطلاعاتی را که منجر به تسریع عملیات گزارش گیری و مغایرت گیری کمک میکند را در این فیلدها ذخیره و هنگام پرداخت به بانک ارسال نمایید. + +
+ + ```php +// At the top of the file. +use Shetabit\Multipay\Invoice; +... + + +// Create new invoice. +$invoice = (new Invoice)->amount(1000); + +// Use invoice bag to store custom field values. +$invoice->detail([ + 'ResNum1' => $order->orderId, + 'ResNum2' => $customer->verifiedCode, + 'ResNum3' => $someValue, + 'ResNum4' => $someOtherValue, + ]); +``` + +
+ +#### رویدادها: **نکته اول:** تمامی listener ها به صورت global تنظیم خواهند شد و برای تمامی پرداخت ها اعمال میشوند. @@ -492,6 +616,8 @@ class MyDriver extends Driver - **purchase**: این رویداد بعد از purchase شدن صورتحساب فراخوانی میشود. +
+ ```php // add purchase event listener Payment::addPurchaseListener(function($driver, $invoice) { @@ -500,7 +626,10 @@ Payment::addPurchaseListener(function($driver, $invoice) { }); ``` +
+ - **pay**: این رویداد بعد از اینکه متد pay فراخوانی شود اتفاق می افتد. در این حالت صورتحساب اماده ی پرداخت می باشد. +
```php // add pay event listener @@ -514,7 +643,11 @@ Payment::addPayListener(function($driver, $invoice) { }); ``` +
+ - **verify**: این رویداد هنگامی که صورتحساب موفقیت آمیز verify شود فراخوانی میشود. +
+ ```php // شما میتوانید چندین لیستنر داشته باشید و همچنین آنها را حذف کنید @@ -537,6 +670,112 @@ Payment::removeVerifyListener($firstListener); Payment::removeVerifyListener(); // remove all verify listeners :D ``` +
+ +## درایور آفلاین + (Local driver) + + +این درایور برای شبیه سازی روند خرید از درگاه اینترنتی استفاده میشود. + +شروع روند پرداخت مانند بقیه درایورها است. + +
+ +```php +$invoice = (new Invoice)->amount(10000); +$payment->via('local')->purchase($invoice, function($driver, $transactionId) { + // یک شناسه پرداخت به صورت اتفاقی تولید و برگردانده میشود +})->pay()->render(); +``` +

+ +
+ +بعد از صدا زدن متد `render` یک فرم `HTML‍` با دکمه های **پرداخت موفق** و **پرداخت ناموفق** نمایش داده میشود. این دکمه‌ها یک پرداخت موفق یا ناموفق درگاه بانکی واقعی را شبیه سازی میکنند و پس از آن مسیر را به callbackUrl انتقال میدهند. + +در هر دو حالت پارامتر `trasactionId` به انتهای مسیر callbackUrl اضافه شده و قابل دسترسی است. + +بعد از انتقال به callbackUrl امکان اعتبارسنجی تراکنش وجود دارد. +
+ +```php +$receipt = $payment->via('local')->verify(); +``` + +
+در صورت پرداخت موفق، رسید پرداخت با مشخصات ساختگی تولید میشود. + +
+ +```php +[ +'orderId' => // شماره سفارش (ساختگی) +'traceNo' => // شماره پیگیری (ساختگی) (جهت ذخیره در دیتابیس) +'referenceNo' => // شماره تراکنش که در مرحله قبل تولید شده بود (transactionId) +'cardNo' => // چهار رقم آخر کارت (ساختگی) +] +``` + +
+در صورتی که پرداخت ناموفق (یا لغو تراکنش)، یک استثنا از نوع `InvalidPaymentException` ایجاد میشود که حاوی پیام لغو تراکنش توسط کاربر است. + +تعدادی از امکانات درایور توسط مقدارهایی که به `invoice` داده میشوند، قابل تنظیم است. +
+ + +- ###### `پارامترهای قابل تنظیم` + +```php +$invoice->detail([ + // setting this value will cause `purchase` method to throw an `PurchaseFailedException` + // to simulate when a gateway can not initialize the payment. + 'failedPurchase' => 'custom message to decribe the error', + + // Setting this parameter will be shown in payment form. + 'orderId' => 4444, +]); +``` + +- ###### `ظاهر فرم` + +
+ بعضی از مشخصات ظاهری فرم پرداخت نمایش داده شده از طریق پارامترهای درایور `local` در فایل `payment.php` قابل تغییر هستند. + +
+ +```php +'local' => [ + // default callback url of the driver + 'callbackUrl' => '/callback', + + // main title of the form + 'title' => 'Test gateway', + + // a description to show under the title for more clarification + 'description' => 'This gateway is for using in development environments only.', + + // custom label to show as order No. + 'orderLabel' => 'Order No.', + + // custom label to show as payable amount + 'amountLabel' => 'Payable amount', + + // custom label of successful payment button + 'payButton' => 'Successful Payment', + + // custom label of cancel payment button + 'cancelButton' => 'Cancel Payment', +], +``` + + + + + + +
+ ## تغییرات برای مشاهده آخرین تغییرات انجام شده در پکیج [قسمت تغییرات](CHANGELOG.md) را بررسی کنید. diff --git a/README-ZH.md b/README-ZH.md index 44bade0..c706830 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -9,14 +9,16 @@ [![Software License][ico-license]](LICENSE.md) [![Latest Version on Packagist][ico-version]][link-packagist] [![Total Downloads on Packagist][ico-download]][link-packagist] -[![StyleCI](https://github.styleci.io/repos/169948762/shield?branch=master)](https://github.styleci.io/repos/169948762) -[![Maintainability](https://api.codeclimate.com/v1/badges/e6a80b17298cb4fcb56d/maintainability)](https://codeclimate.com/github/shetabit/multipay/maintainability) +[![StyleCI](https://github.styleci.io/repos/268039684/shield?branch=master)](https://github.styleci.io/repos/268039684) +[![Maintainability](https://api.codeclimate.com/v1/badges/3aa790c544c9f2132b16/maintainability)](https://codeclimate.com/github/shetabit/multipay/maintainability) [![Quality Score][ico-code-quality]][link-code-quality] 这是一个用于整合支付网关的PHP包。这个包依赖 `PHP 7.2+`. [捐赠我](https://yekpay.me/mahdikhanzadi) 如果你喜欢这个包:sunglasses: :bowtie: +For **Laravel** integration you can use [shetabit/payment](https://github.com/shetabit/payment) package. + > 此软件包可用于多个驱动程序,如果在[当前驱动程序列表](#list-of-available-drivers)中找不到驱动程序,则可以创建它们 - [داکیومنت فارسی][link-fa] @@ -45,20 +47,43 @@ - [License](#license) # 可用驱动列表 -- [asanpardakht](https://asanpardakht.ir/) :warning: (未测试) + +- [aqayepardakht](https://aqayepardakht.ir/) :heavy_check_mark: +- [asanpardakht](https://asanpardakht.ir/) :heavy_check_mark: +- [atipay](https://www.atipay.net/) :heavy_check_mark: +- [azkiVam (Installment payment)](https://www.azkivam.com/) :heavy_check_mark: - [behpardakht (mellat)](http://www.behpardakht.com/) :heavy_check_mark: +- [bitpay](https://bitpay.ir/) :heavy_check_mark: +- [digipay](https://www.mydigipay.com/) :heavy_check_mark: +- [etebarino (Installment payment)](https://etebarino.com/) :heavy_check_mark: +- [fanavacard](https://www.fanava.com/) :heavy_check_mark: +- [gooyapay](https://gooyapay.ir/) :heavy_check_mark: - [idpay](https://idpay.ir/) :heavy_check_mark: - [irankish](http://irankish.com/) :heavy_check_mark: +- [jibit](https://jibit.ir/) :heavy_check_mark: +- [local](#local-driver) :heavy_check_mark: +- [minipay](https://minipay.me/) :heavy_check_mark: - [nextpay](https://nextpay.ir/) :heavy_check_mark: +- [omidpay](https://omidpayment.ir/) :heavy_check_mark: - [parsian](https://www.pec.ir/) :heavy_check_mark: - [pasargad](https://bpi.ir/) :heavy_check_mark: +- [payfa](https://payfa.com/) :heavy_check_mark: - [payir](https://pay.ir/) :heavy_check_mark: -- [paypal](http://www.paypal.com/) (在下一个版本中很快就支持了) +- [paypal](http://www.paypal.com/) (will be added soon in next version) - [payping](https://www.payping.ir/) :heavy_check_mark: - [paystar](http://paystar.ir/) :heavy_check_mark: - [poolam](https://poolam.ir/) :heavy_check_mark: +- [rayanpay](https://rayanpay.com/) :heavy_check_mark: - [sadad (melli)](https://sadadpsp.ir/) :heavy_check_mark: - [saman](https://www.sep.ir) :heavy_check_mark: +- [sep (saman electronic payment) Keshavarzi & Saderat](https://www.sep.ir) :heavy_check_mark: +- [sepehr (saderat)](https://www.sepehrpay.com/) :heavy_check_mark: +- [sepordeh](https://sepordeh.com/) :heavy_check_mark: +- [sizpay](https://www.sizpay.ir/) :heavy_check_mark: +- [toman](https://tomanpay.net/) :heavy_check_mark: +- [snapppay](https://snapppay.ir/) :heavy_check_mark: +- [vandar](https://vandar.io/) :heavy_check_mark: +- [walleta (Installment payment)](https://walleta.ir/) :heavy_check_mark: - [yekpay](https://yekpay.com/) :heavy_check_mark: - [zarinpal](https://www.zarinpal.com/) :heavy_check_mark: - [zibal](https://www.zibal.ir/) :heavy_check_mark: @@ -76,7 +101,6 @@ - wepay - payoneer - paysimple -- saderat > 如果找不到你需要的,您可以创建你自己的驱动,阅读`创建自定义驱动`部分,可以了解更多 @@ -122,7 +146,7 @@ b. 在配置文件中,您可以将 `default`设置项设置为你希望的付 c. Instantiate the `Payment` class and **pass configs to it** like the below: ```php - use Shetabit\MultipayMultipay; + use Shetabit\Multipay\Payment // load the config file from your project $paymentConfig = require('path/to/payment.php'); @@ -184,24 +208,30 @@ $invoice->detail('detailName1','your detail1 goes here') ```php // At the top of the file. use Shetabit\Multipay\Invoice; -use Shetabit\Multipay\Facade\Payment; +use Shetabit\Multipay\Payment; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase the given invoice. -Payment::purchase($invoice,function($driver, $transactionId) { +$payment->purchase($invoice,function($driver, $transactionId) { // We can store $transactionId in database. }); // Purchase method accepts a callback function. -Payment::purchase($invoice, function($driver, $transactionId) { +$payment->purchase($invoice, function($driver, $transactionId) { // We can store $transactionId in database. }); // You can specify callbackUrl -Payment::callbackUrl('http://yoursite.com/verify')->purchase( +$payment->callbackUrl('http://yoursite.com/verify')->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. @@ -216,19 +246,25 @@ Payment::callbackUrl('http://yoursite.com/verify')->purchase( ```php // At the top of the file. use Shetabit\Multipay\Invoice; -use Shetabit\Multipay\Facade\Payment; +use Shetabit\Multipay\Payment; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase and pay the given invoice. // You should use return statement to redirect user to the bank page. -return Payment::purchase($invoice, function($driver, $transactionId) { +return $payment->purchase($invoice, function($driver, $transactionId) { // Store transactionId in database as we need it to verify payment in the future. })->pay()->render(); // Do all things together in a single line. -return Payment::purchase( +return $payment->purchase( (new Invoice)->amount(1000), function($driver, $transactionId) { // 把交易ID保存到数据库. @@ -237,7 +273,7 @@ return Payment::purchase( })->pay()->render(); // Retrieve json format of Redirection (in this case you can handle redirection to bank gateway) -return Payment::purchase( +return $payment->purchase( (new Invoice)->amount(1000), function($driver, $transactionId) { // 把交易ID保存到数据库. @@ -252,15 +288,21 @@ return Payment::purchase( ```php // At the top of the file. -use Shetabit\Multipay\Facade\Payment; +use Shetabit\Multipay\Payment; use Shetabit\Multipay\Exceptions\InvalidPaymentException; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // 您需要验证支付机构的回传数据,以确保付款成功 // 我们需要使用交易ID来验证 // 使用交易金额来验证,也是一个很好的方法 try { - $receipt = Payment::amount(1000)->transactionId($transaction_id)->verify(); + $receipt = $payment->amount(1000)->transactionId($transaction_id)->verify(); // You can show payment referenceId to the user. echo $receipt->getReferenceId(); @@ -283,8 +325,14 @@ try { ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\Facade\Payment; + use Shetabit\Multipay\Payment; ... + + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + // Create new invoice. $invoice = (new Invoice)->amount(1000); @@ -303,11 +351,17 @@ try { ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\Facade\Payment; + use Shetabit\Multipay\Payment; ... + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Purchase (we set invoice to null). - Payment::callbackUrl($url)->amount(1000)->purchase( + $payment->callbackUrl($url)->amount(1000)->purchase( null, function($driver, $transactionId) { // We can store $transactionId in database. @@ -320,14 +374,20 @@ try { ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\Facade\Payment; + use Shetabit\Multipay\Payment; ... + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase the given invoice. - Payment::via('driverName')->purchase( + $payment->via('driverName')->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. @@ -340,14 +400,20 @@ try { ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\Facade\Payment; + use Shetabit\Multipay\Payment; ... + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase the given invoice with custom driver configs. - Payment::config('mechandId', 'your mechand id')->purchase( + $payment->config('mechandId', 'your mechand id')->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. @@ -355,7 +421,7 @@ try { ); // Also we can change multiple configs at the same time. - Payment::config(['key1' => 'value1', 'key2' => 'value2'])->purchase( + $payment->config(['key1' => 'value1', 'key2' => 'value2'])->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. @@ -455,11 +521,11 @@ class MyDriver extends Driver ] ``` -**Note:-** 必须确保 `map` 数组的键与 `drivers` 数组的键相同。 +**Note:** 必须确保 `map` 数组的键与 `drivers` 数组的键相同。 -#### 事件 +#### 事件: -**Notice 1: **event listeners will be registered globaly for all payments. +**Notice 1:** event listeners will be registered globaly for all payments. **Notice 2:** if you want your listeners work correctly, you **must** subcribe them before the target event dispatches. diff --git a/README.md b/README.md index 69ef949..cc826cd 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,21 @@

- - # PHP Payment Gateway - - [![Software License][ico-license]](LICENSE.md) [![Latest Version on Packagist][ico-version]][link-packagist] [![Total Downloads on Packagist][ico-download]][link-packagist] -[![StyleCI](https://github.styleci.io/repos/169948762/shield?branch=master)](https://github.styleci.io/repos/169948762) -[![Maintainability](https://api.codeclimate.com/v1/badges/e6a80b17298cb4fcb56d/maintainability)](https://codeclimate.com/github/shetabit/multipay/maintainability) +[![StyleCI](https://github.styleci.io/repos/268039684/shield?branch=master)](https://github.styleci.io/repos/268039684) +[![Maintainability](https://api.codeclimate.com/v1/badges/3aa790c544c9f2132b16/maintainability)](https://codeclimate.com/github/shetabit/multipay/maintainability) [![Quality Score][ico-code-quality]][link-code-quality] This is a PHP Package for Payment Gateway Integration. This package supports `PHP 7.2+`. [Donate me](https://yekpay.me/mahdikhanzadi) if you like this package :sunglasses: :bowtie: -> This packages works with multiple drivers, and you can create custom drivers if you can't find them in the [current drivers list](#list-of-available-drivers) (below list). +For **Laravel** integration you can use [shetabit/payment](https://github.com/shetabit/payment) package. + +> This package works with multiple drivers, and you can create custom drivers if you can't find them in the [current drivers list](#list-of-available-drivers) (below list). - [داکیومنت فارسی][link-fa] - [English documents][link-en] @@ -31,13 +29,14 @@ This is a PHP Package for Payment Gateway Integration. This package supports `PH - [Install](#install) - [Configure](#configure) - [How to use](#how-to-use) - - [Working with invoices](#working-with-invoices) - - [Purchase invoice](#purchase-invoice) - - [Pay invoice](#pay-invoice) - - [Verify payment](#verify-payment) - - [Useful methods](#useful-methods) - - [Create custom drivers:](#create-custom-drivers) - - [Events](#events) + - [Working with invoices](#working-with-invoices) + - [Purchase invoice](#purchase-invoice) + - [Pay invoice](#pay-invoice) + - [Verify payment](#verify-payment) + - [Useful methods](#useful-methods) + - [Create custom drivers:](#create-custom-drivers) + - [Events](#events) + - [Local driver (for development)](#local-driver) - [Change log](#change-log) - [Contributing](#contributing) - [Security](#security) @@ -45,20 +44,43 @@ This is a PHP Package for Payment Gateway Integration. This package supports `PH - [License](#license) # List of available drivers -- [asanpardakht](https://asanpardakht.ir/) :warning: (not tested) + +- [aqayepardakht](https://aqayepardakht.ir/) :heavy_check_mark: +- [asanpardakht](https://asanpardakht.ir/) :heavy_check_mark: +- [atipay](https://www.atipay.net/) :heavy_check_mark: +- [azkiVam (Installment payment)](https://www.azkivam.com/) :heavy_check_mark: - [behpardakht (mellat)](http://www.behpardakht.com/) :heavy_check_mark: +- [bitpay](https://bitpay.ir/) :heavy_check_mark: +- [digipay](https://www.mydigipay.com/) :heavy_check_mark: +- [etebarino (Installment payment)](https://etebarino.com/) :heavy_check_mark: +- [fanavacard](https://www.fanava.com/) :heavy_check_mark: +- [gooyapay](https://gooyapay.ir/) :heavy_check_mark: - [idpay](https://idpay.ir/) :heavy_check_mark: - [irankish](http://irankish.com/) :heavy_check_mark: +- [jibit](https://jibit.ir/) :heavy_check_mark: +- [local](#local-driver) :heavy_check_mark: +- [minipay](https://minipay.me/) :heavy_check_mark: - [nextpay](https://nextpay.ir/) :heavy_check_mark: +- [omidpay](https://omidpayment.ir/) :heavy_check_mark: - [parsian](https://www.pec.ir/) :heavy_check_mark: - [pasargad](https://bpi.ir/) :heavy_check_mark: +- [payfa](https://payfa.com/) :heavy_check_mark: - [payir](https://pay.ir/) :heavy_check_mark: - [paypal](http://www.paypal.com/) (will be added soon in next version) - [payping](https://www.payping.ir/) :heavy_check_mark: - [paystar](http://paystar.ir/) :heavy_check_mark: - [poolam](https://poolam.ir/) :heavy_check_mark: +- [rayanpay](https://rayanpay.com/) :heavy_check_mark: - [sadad (melli)](https://sadadpsp.ir/) :heavy_check_mark: - [saman](https://www.sep.ir) :heavy_check_mark: +- [sep (saman electronic payment) Keshavarzi & Saderat](https://www.sep.ir) :heavy_check_mark: +- [sepehr (saderat)](https://www.sepehrpay.com/) :heavy_check_mark: +- [sepordeh](https://sepordeh.com/) :heavy_check_mark: +- [sizpay](https://www.sizpay.ir/) :heavy_check_mark: +- [snapppay](https://snapppay.ir/) :heavy_check_mark: +- [toman](https://tomanpay.net/) :heavy_check_mark: +- [vandar](https://vandar.io/) :heavy_check_mark: +- [walleta (Installment payment)](https://walleta.ir/) :heavy_check_mark: - [yekpay](https://yekpay.com/) :heavy_check_mark: - [zarinpal](https://www.zarinpal.com/) :heavy_check_mark: - [zibal](https://www.zibal.ir/) :heavy_check_mark: @@ -76,23 +98,22 @@ This is a PHP Package for Payment Gateway Integration. This package supports `PH - wepay - payoneer - paysimple -- saderat -> you can create your own custom drivers if it's not exists in the list, read the `Create custom drivers` section. +> you can create your own custom drivers if it doesn't exist in the list, read the `Create custom drivers` section. ## Install Via Composer -``` bash +```bash $ composer require shetabit/multipay ``` ## Configure -a. Copy `config/payment.php` and put it somewhere in your project. (you can also find it in `vendor/shetabit/multipay/config/payment.php` path). +a. Copy `config/payment.php` into somewhere in your project. (you can also find it in `vendor/shetabit/multipay/config/payment.php` path). -b. In the config file you can set the `default driver` to use for all your payments. But you can also change the driver at runtime. +b. In the config file you can set the `default driver` to be used for all your payments and you can also change the driver at runtime. Choose what gateway you would like to use in your application. Then make that as default driver so that you don't have to specify that everywhere. But, you can also use multiple gateways in a project. @@ -121,7 +142,7 @@ Then fill the credentials for that gateway in the drivers array. c. Instantiate the `Payment` class and **pass configs to it** like the below: ```php - use Shetabit\MultipayMultipay; + use Shetabit\Multipay\Payment; // load the config file from your project $paymentConfig = require('path/to/payment.php'); @@ -131,13 +152,12 @@ c. Instantiate the `Payment` class and **pass configs to it** like the below: ## How to use -your `Invoice` holds your payment details, so initially we'll talk about `Invoice` class. +your `Invoice` holds your payment details, so initially we'll talk about `Invoice` class. #### Working with invoices before doing any thing you need to use `Invoice` class to create an invoice. - In your code, use it like the below: ```php @@ -161,14 +181,14 @@ $invoice->detail(['name1' => 'detail1','name2' => 'detail2']); // 4 $invoice->detail('detailName1','your detail1 goes here') ->detail('detailName2','your detail2 goes here'); - ``` -available methods: + +Available methods: - `uuid`: set the invoice unique id - `getUuid`: retrieve the invoice current unique id - `detail`: attach some custom details into invoice -- `getDetails`: retrieve all custom details +- `getDetails`: retrieve all custom details - `amount`: set the invoice amount - `getAmount`: retrieve invoice amount - `transactionId`: set invoice payment transaction id @@ -177,31 +197,38 @@ available methods: - `getDriver`: retrieve the driver #### Purchase invoice + In order to pay the invoice, we need the payment transactionId. We purchase the invoice to retrieve transaction id: ```php // At the top of the file. use Shetabit\Multipay\Invoice; -use Shetabit\Multipay\FacadeMultipay; +use Shetabit\Multipay\Payment; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); // Purchase the given invoice. -Payment::purchase($invoice,function($driver, $transactionId) { +$payment->purchase($invoice,function($driver, $transactionId) { // We can store $transactionId in database. }); // Purchase method accepts a callback function. -Payment::purchase($invoice, function($driver, $transactionId) { +$payment->purchase($invoice, function($driver, $transactionId) { // We can store $transactionId in database. }); // You can specify callbackUrl -Payment::callbackUrl('http://yoursite.com/verify')->purchase( - $invoice, +$payment->callbackUrl('http://yoursite.com/verify')->purchase( + $invoice, function($driver, $transactionId) { // We can store $transactionId in database. } @@ -215,19 +242,26 @@ After purchasing the invoice, we can redirect the user to the bank payment page: ```php // At the top of the file. use Shetabit\Multipay\Invoice; -use Shetabit\Multipay\FacadeMultipay; +use Shetabit\Multipay\Payment; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); + // Purchase and pay the given invoice. // You should use return statement to redirect user to the bank page. -return Payment::purchase($invoice, function($driver, $transactionId) { +return $payment->purchase($invoice, function($driver, $transactionId) { // Store transactionId in database as we need it to verify payment in the future. })->pay()->render(); // Do all things together in a single line. -return Payment::purchase( +return $payment->purchase( (new Invoice)->amount(1000), function($driver, $transactionId) { // Store transactionId in database. @@ -236,7 +270,7 @@ return Payment::purchase( )->pay()->render(); // Retrieve json format of Redirection (in this case you can handle redirection to bank gateway) -return Payment::purchase( +return $payment->purchase( (new Invoice)->amount(1000), function($driver, $transactionId) { // Store transactionId in database. @@ -251,15 +285,21 @@ When user has completed the payment, the bank redirects them to your website, th ```php // At the top of the file. -use Shetabit\Multipay\FacadeMultipay; +use Shetabit\Multipay\Payment; use Shetabit\Multipay\Exceptions\InvalidPaymentException; ... +// load the config file from your project +$paymentConfig = require('path/to/payment.php'); + +$payment = new Payment($paymentConfig); + + // You need to verify the payment to ensure the invoice has been paid successfully. // We use transaction id to verify payments // It is a good practice to add invoice amount as well. try { - $receipt = Payment::amount(1000)->transactionId($transaction_id)->verify(); + $receipt = $payment->amount(1000)->transactionId($transaction_id)->verify(); // You can show payment referenceId to the user. echo $receipt->getReferenceId(); @@ -279,74 +319,99 @@ try { - ###### `callbackUrl`: can be used to change callbackUrl on the runtime. + ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\FacadeMultipay; + use Shetabit\Multipay\Payment; ... - + + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); - + // Purchase the given invoice. - Payment::callbackUrl($url)->purchase( + $payment->callbackUrl($url)->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. } ); ``` - - ###### `amount`: you can set the invoice amount directly + ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\FacadeMultipay; + use Shetabit\Multipay\Payment; ... - + + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Purchase (we set invoice to null). - Payment::callbackUrl($url)->amount(1000)->purchase( - null, + $payment->callbackUrl($url)->amount(1000)->purchase( + null, function($driver, $transactionId) { // We can store $transactionId in database. } ); ``` - - ###### `via`: change driver on the fly + ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\FacadeMultipay; + use Shetabit\Multipay\Payment; ... - + + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); - + // Purchase the given invoice. - Payment::via('driverName')->purchase( + $payment->via('driverName')->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. } ); ``` - - ###### `config`: set driver configs on the fly + ```php // At the top of the file. use Shetabit\Multipay\Invoice; - use Shetabit\Multipay\FacadeMultipay; + use Shetabit\Multipay\Payment; ... - + + // load the config file from your project + $paymentConfig = require('path/to/payment.php'); + + $payment = new Payment($paymentConfig); + + // Create new invoice. $invoice = (new Invoice)->amount(1000); - + // Purchase the given invoice with custom driver configs. - Payment::config('mechandId', 'your mechand id')->purchase( + $payment->config('mechandId', 'your mechand id')->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. @@ -354,13 +419,34 @@ try { ); // Also we can change multiple configs at the same time. - Payment::config(['key1' => 'value1', 'key2' => 'value2'])->purchase( + $payment->config(['key1' => 'value1', 'key2' => 'value2'])->purchase( $invoice, function($driver, $transactionId) { // We can store $transactionId in database. } ); ``` +- `custom fileds`: Use custom fields of gateway (Not all gateways support this feature) + SEP gateway support up to 4 custom fields and you can set the value to a string up to 50 characters. + These custom fields are shown only when viewing reports in the user's panel. + + ```php + // At the top of the file. + use Shetabit\Multipay\Invoice; + ... + + + // Create new invoice. + $invoice = (new Invoice)->amount(1000); + + // Use invoice bag to store custom field values. + $invoice->detail([ + 'ResNum1' => $order->orderId, + 'ResNum2' => $customer->verifiedCode, + 'ResNum3' => $someValue, + 'ResNum4' => $someOtherValue, + ]); + ``` #### Create custom drivers: @@ -424,20 +510,20 @@ class MyDriver extends Driver return $this->redirectWithForm($url, $inputs, $method); } - + // Verify the payment (we must verify to ensure that user has paid the invoice). public function verify(): ReceiptInterface { $verifyPayment = $this->settings->verifyApiUrl; - + $verifyUrl = $verifyPayment.$this->invoice->getTransactionId(); - + ... - + /** Then we send a request to $verifyUrl and if payment is not valid we throw an InvalidPaymentException with a suitable message. **/ throw new InvalidPaymentException('a suitable message'); - + /** We create a receipt for this payment if everything goes normally. **/ @@ -455,11 +541,11 @@ Once you create that class you have to specify it in the `payment.php` config fi ] ``` -**Note:-** You have to make sure that the key of the `map` array is identical to the key of the `drivers` array. +**Note:** You have to make sure that the key of the `map` array is identical to the key of the `drivers` array. -#### Events +#### Events: -**Notice 1: **event listeners will be registered globaly for all payments. +**Notice 1:** event listeners will be registered globaly for all payments. **Notice 2:** if you want your listeners work correctly, you **must** subcribe them before the target event dispatches. @@ -467,7 +553,7 @@ Once you create that class you have to specify it in the `payment.php` config fi --- -You can listen for 3 events: +You can listen for 3 events: 1. **purchase** 2. **pay** @@ -520,6 +606,87 @@ Payment::removeVerifyListener($firstListener); Payment::removeVerifyListener(); // remove all verify listeners :D ``` +## Local driver + +`Local` driver can simulate payment flow of a real gateway for development purpose. + +Payment can be initiated like any other driver + +```php +$invoice = (new Invoice)->amount(10000); +$payment->via('local')->purchase($invoice, function($driver, $transactionId) { + // a fake transaction ID is generated and returned. +})->pay()->render(); +``` + +

+ +Calling `render()` method will render a `HTML` form with **Accept** and **Cancel** buttons, which simulate corresponding action of real payment gateway. and redirects to the specified callback url. +`transactionId` parameter will allways be available in the returned query url. + +Payment can be verified after receiving the callback request. + +```php +$receipt = $payment->via('local')->verify(); +``` + +In case of succesful payment, `$receipt` will contains the following parameters + +```php +[ +'orderId' => // fake order number +'traceNo' => // fake trace number (this should be stored in databse) +'referenceNo' => // generated transaction ID in `purchase` method callback +'cardNo' => // fake last four digits of card +] +``` + +In case of canceled payment, `PurchaseFailedException` will be thrown to simulate the failed verification of gateway. + +Driver functionalities can be configured via `Invoice` detail bag. + +- ###### `available parameters` + +```php +$invoice->detail([ + // setting this value will cause `purchase` method to throw an `PurchaseFailedException` + // to simulate when a gateway can not initialize the payment. + 'failedPurchase' => 'custom message to decribe the error', + + // Setting this parameter will be shown in payment form. + 'orderId' => 4444, +]); +``` + +- ###### `appearance` + +Appearance of payment form can be customized via config parameter of `local` driver in `payment.php` file. + +```php +'local' => [ + // default callback url of the driver + 'callbackUrl' => '/callback', + + // main title of the form + 'title' => 'Test gateway', + + // a description to show under the title for more clarification + 'description' => 'This gateway is for using in development environments only.', + + // custom label to show as order No. + 'orderLabel' => 'Order No.', + + // custom label to show as payable amount + 'amountLabel' => 'Payable amount', + + // custom label of successful payment button + 'payButton' => 'Successful Payment', + + // custom label of cancel payment button + 'cancelButton' => 'Cancel Payment', +], +``` + ## Change log Please see [CHANGELOG](CHANGELOG.md) for more information on what has been changed recently. @@ -545,7 +712,6 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio [ico-download]: https://img.shields.io/packagist/dt/shetabit/multipay.svg?color=%23F18&style=flat-square [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square [ico-code-quality]: https://img.shields.io/scrutinizer/g/shetabit/multipay.svg?label=Code%20Quality&style=flat-square - [link-fa]: README-FA.md [link-en]: README.md [link-zh]: README-ZH.md diff --git a/composer.json b/composer.json index 12c9c12..583c784 100755 --- a/composer.json +++ b/composer.json @@ -13,6 +13,8 @@ "iranKish payment gateway", "php iranKish package", "php iranKish", + "asan pardakht", + "php asan pardakht", "irankish", "jahanPay payment gateway", "php jahanPay package", @@ -85,9 +87,11 @@ ], "require": { "php": ">=7.2", - "nesbot/carbon": "^1.39|^2.0", + "chillerlan/php-cache": "^4.1|^5.0", "guzzlehttp/guzzle": ">=6.2", - "ramsey/uuid": "^3.7|^3.8|^3.9|^4.0" + "nesbot/carbon": "^1.39|^2.0|^3.0", + "ramsey/uuid": "^3.7|^3.8|^3.9|^4.0", + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "^8.0|^9.0", diff --git a/config/payment.php b/config/payment.php index 37ae7c1..9a915cf 100755 --- a/config/payment.php +++ b/config/payment.php @@ -23,16 +23,50 @@ | */ 'drivers' => [ + 'local' => [ + 'callbackUrl' => '/callback', + 'title' => 'درگاه پرداخت تست', + 'description' => 'این درگاه *صرفا* برای تست صحت روند پرداخت و لغو پرداخت میباشد', + 'orderLabel' => 'شماره سفارش', + 'amountLabel' => 'مبلغ قابل پرداخت', + 'payButton' => 'پرداخت موفق', + 'cancelButton' => 'پرداخت ناموفق', + ], + 'gooyapay' => [ + 'apiPurchaseUrl' => 'https://gooyapay.ir/webservice/rest/PaymentRequest', + 'apiVerificationUrl' => 'https://gooyapay.ir/webservice/rest/PaymentVerification', + 'apiPaymentUrl' => 'https://gooyapay.ir/startPay/', + 'merchantId' => 'XXXX-XXXX-XXXX-XXXXXXXXXXXXXXXXXXXXX', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'fanavacard' => [ + 'baseUri' => 'https://fcp.shaparak.ir', + 'apiPaymentUrl' => '_ipgw_//payment/', + 'apiPurchaseUrl' => 'ref-payment/RestServices/mts/generateTokenWithNoSign/', + 'apiVerificationUrl' => 'ref-payment/RestServices/mts/verifyMerchantTrans/', + 'apiReverseAmountUrl' => 'ref-payment/RestServices/mts/reverseMerchantTrans/', + 'username' => 'xxxxxxx', + 'password' => 'xxxxxxx', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'atipay' => [ + 'atipayTokenUrl' => 'https://mipg.atipay.net/v1/get-token', + 'atipayRedirectGatewayUrl' => 'https://mipg.atipay.net/v1/redirect-to-gateway', + 'atipayVerifyUrl' => 'https://mipg.atipay.net/v1/verify-payment', + 'apikey' => '', + 'currency' => 'R', //Can be R, T (Rial, Toman) + 'callbackUrl' => 'http://yoursite.com/path/to', + 'description' => 'payment using Atipay', + ], 'asanpardakht' => [ - 'apiPurchaseUrl' => 'https://services.asanpardakht.net/paygate/merchantservices.asmx?wsdl', 'apiPaymentUrl' => 'https://asan.shaparak.ir', - 'apiVerificationUrl' => 'https://services.asanpardakht.net/paygate/merchantservices.asmx?wsdl', - 'apiUtilsUrl' => 'https://services.asanpardakht.net/paygate/internalutils.asmx?wsdl', - 'key' => '', - 'iv' => '', + 'apiRestPaymentUrl' => 'https://ipgrest.asanpardakht.ir/v1/', 'username' => '', 'password' => '', - 'merchantId' => '', + 'merchantConfigID' => '', + 'currency' => 'T', //Can be R, T (Rial, Toman) 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using asanpardakht', ], @@ -40,12 +74,33 @@ 'apiPurchaseUrl' => 'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl', 'apiPaymentUrl' => 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat', 'apiVerificationUrl' => 'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl', - 'apiNamespaceUrl' => 'http://interfaces.core.sw.bps.com/', 'terminalId' => '', 'username' => '', 'password' => '', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using behpardakht', + 'currency' => 'T', //Can be R, T (Rial, Toman) + 'cumulativeDynamicPayStatus' => false, + ], + 'digipay' => [ + 'apiPaymentUrl' => 'https://api.mydigipay.com', // with out '/' at the end + 'username' => 'username', + 'password' => 'password', + 'client_id' => '', + 'client_secret' => '', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'currency' => 'R', //Can be R, T (Rial, Toman) + ], + 'etebarino' => [ + 'apiPurchaseUrl' => 'https://api.etebarino.com/public/merchant/request-payment', + 'apiPaymentUrl' => 'https://panel.etebarino.com/gateway/public/ipg', + 'apiVerificationUrl' => 'https://api.etebarino.com/public/merchant/verify-payment', + 'merchantId' => '', + 'terminalId' => '', + 'username' => '', + 'password' => '', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'description' => 'payment using etebarino', ], 'idpay' => [ 'apiPurchaseUrl' => 'https://api.idpay.ir/v1.1/payment', @@ -56,23 +111,50 @@ 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using idpay', 'sandbox' => false, // set it to true for test environments + 'currency' => 'R', //Can be R, T (Rial, Toman) ], 'irankish' => [ - 'apiPurchaseUrl' => 'https://ikc.shaparak.ir/XToken/Tokens.xml', - 'apiPaymentUrl' => 'https://ikc.shaparak.ir/TPayment/Payment/index/', - 'apiVerificationUrl' => 'https://ikc.shaparak.ir/XVerify/Verify.xml', - 'merchantId' => '', - 'sha1Key' => '', + 'apiPurchaseUrl' => 'https://ikc.shaparak.ir/api/v3/tokenization/make', + 'apiPaymentUrl' => 'https://ikc.shaparak.ir/iuiv3/IPG/Index/', + 'apiVerificationUrl' => 'https://ikc.shaparak.ir/api/v3/confirmation/purchase', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using irankish', + 'terminalId' => '', + 'password' => '', + 'acceptorId' => '', + 'pubKey' => '', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'jibit' => [ + 'apiPaymentUrl' => 'https://napi.jibit.ir/ppg/v3', + 'apiKey' => '', + 'apiSecret' => '', + // You can change the token storage path in Laravel like this + // 'tokenStoragePath' => function_exists('storage_path') ? storage_path('jibit/') : 'jibit/' + 'tokenStoragePath' => 'jibit/', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'description' => 'payment using jibit', + 'currency' => 'T', // Can be R, T (Rial, Toman) ], 'nextpay' => [ - 'apiPurchaseUrl' => 'https://api.nextpay.org/gateway/token.http', - 'apiPaymentUrl' => 'https://api.nextpay.org/gateway/payment/', - 'apiVerificationUrl' => 'https://api.nextpay.org/gateway/verify.http', + 'apiPurchaseUrl' => 'https://nextpay.org/nx/gateway/token', + 'apiPaymentUrl' => 'https://nextpay.org/nx/gateway/payment/', + 'apiVerificationUrl' => 'https://nextpay.org/nx/gateway/verify', 'merchantId' => '', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using nextpay', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'omidpay' => [ + 'apiGenerateTokenUrl' => 'https://ref.sayancard.ir/ref-payment/RestServices/mts/generateTokenWithNoSign/', + 'apiPaymentUrl' => 'https://say.shaparak.ir/_ipgw_/MainTemplate/payment/', + 'apiVerificationUrl' => 'https://ref.sayancard.ir/ref-payment/RestServices/mts/verifyMerchantTrans/', + 'username' => '', + 'merchantId' => '', + 'password' => '', + 'callbackUrl' => '', + 'description' => 'payment using omidpay', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'parsian' => [ 'apiPurchaseUrl' => 'https://pec.shaparak.ir/NewIPGServices/Sale/SaleService.asmx?wsdl', @@ -81,25 +163,30 @@ 'merchantId' => '', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using parsian', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'pasargad' => [ - 'apiPaymentUrl' => 'https://pep.shaparak.ir/payment.aspx', - 'apiGetToken' => 'https://pep.shaparak.ir/Api/v1/Payment/GetToken', - 'apiCheckTransactionUrl' => 'https://pep.shaparak.ir/Api/v1/Payment/CheckTransactionResult', - 'apiVerificationUrl' => 'https://pep.shaparak.ir/Api/v1/Payment/VerifyPayment', + 'apiGetToken' => 'https://pep.shaparak.ir/dorsa1/token/getToken', + 'confirmTransactions' => 'https://pep.shaparak.ir/dorsa1/api/payment/confirm-transactions', + 'verifyPayment' => 'https://pep.shaparak.ir/dorsa1//api/payment/verify-payment', + 'apiPaymentUrl' => 'https://pep.shaparak.ir/dorsa1/api/payment/purchase', + 'paymentInquiry' => 'https://pep.shaparak.ir/dorsa1/api/payment/payment-inquiry', + 'apiBaseUrl' => 'https://pep.shaparak.ir/dorsa1/', 'merchantId' => '', 'terminalCode' => '', - 'certificate' => '', // can be string (and set certificateType to xml_string) or an xml file path (and set cetificateType to xml_file) - 'certificateType' => 'xml_file', // can be: xml_file, xml_string - 'callbackUrl' => 'http://yoursite.com/path/to', + 'username' => '', + 'password' => '', + 'callbackUrl' => '', + 'currency' => 'T' ////Can be R, T (Rial, Toman) ], 'payir' => [ - 'apiPurchaseUrl' => 'https://pay.ir/pg/send/', + 'apiPurchaseUrl' => 'https://pay.ir/pg/send', 'apiPaymentUrl' => 'https://pay.ir/pg/', - 'apiVerificationUrl' => 'https://pay.ir/pg/verify/', + 'apiVerificationUrl' => 'https://pay.ir/pg/verify', 'merchantId' => 'test', // set it to `test` for test environments 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using payir', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'paypal' => [ /* normal api */ @@ -113,26 +200,29 @@ 'sandboxApiVerificationUrl' => 'https://sandbox.zarinpal.com/pg/services/WebGate/wsdl', 'mode' => 'normal', // can be normal, sandbox - 'currency' => '', 'id' => '', // Specify the email of the PayPal Business account 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using paypal', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'payping' => [ - 'apiPurchaseUrl' => 'https://api.payping.ir/v1/pay/', - 'apiPaymentUrl' => 'https://api.payping.ir/v1/pay/gotoipg/', - 'apiVerificationUrl' => 'https://api.payping.ir/v1/pay/verify/', + 'apiPurchaseUrl' => 'https://api.payping.ir/v2/pay/', + 'apiPaymentUrl' => 'https://api.payping.ir/v2/pay/gotoipg/', + 'apiVerificationUrl' => 'https://api.payping.ir/v2/pay/verify/', 'merchantId' => '', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using payping', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'paystar' => [ - 'apiPurchaseUrl' => 'https://paystar.ir/api/create/', - 'apiPaymentUrl' => 'https://paystar.ir/paying/', - 'apiVerificationUrl' => 'https://paystar.ir/api/verify/', - 'merchantId' => '', + 'apiPurchaseUrl' => 'https://core.paystar.ir/api/pardakht/create/', + 'apiPaymentUrl' => 'https://core.paystar.ir/api/pardakht/payment/', + 'apiVerificationUrl' => 'https://core.paystar.ir/api/pardakht/verify/', + 'gatewayId' => '', // your gateway id + 'signKey' => '', // sign key of your gateway 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using paystar', + 'currency' => 'R', //Can be R, T (Rial, Toman) ], 'poolam' => [ 'apiPurchaseUrl' => 'https://poolam.ir/invoice/request/', @@ -141,15 +231,28 @@ 'merchantId' => '', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using poolam', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'sadad' => [ - 'apiPurchaseUrl' => 'https://sadad.shaparak.ir/vpg/api/v0/Request/PaymentRequest', - 'apiPaymentUrl' => 'https://sadad.shaparak.ir/VPG/Purchase', + 'apiPaymentByMultiIdentityUrl' => 'https://sadad.shaparak.ir/VPG/api/v0/PaymentByMultiIdentityRequest', + 'apiPaymentByIdentityUrl' => 'https://sadad.shaparak.ir/api/v0/PaymentByIdentity/PaymentRequest', + 'apiPaymentUrl' => 'https://sadad.shaparak.ir/api/v0/Request/PaymentRequest', + 'apiPurchaseUrl' => 'https://sadad.shaparak.ir/Purchase', 'apiVerificationUrl' => 'https://sadad.shaparak.ir/VPG/api/v0/Advice/Verify', 'key' => '', 'merchantId' => '', 'terminalId' => '', - 'callbackUrl' => 'http://yoursite.com/path/to', + 'callbackUrl' => '', + 'currency' => 'T', //Can be R, T (Rial, Toman) + 'mode' => 'normal', // can be normal, PaymentByIdentity, PaymentByMultiIdentity, + 'PaymentIdentity' => '', + 'MultiIdentityRows' => [ + [ + "IbanNumber" => '', // Sheba number (with IR) + "Amount" => 0, + "PaymentIdentity" => '', + ], + ], 'description' => 'payment using sadad', ], 'saman' => [ @@ -159,6 +262,34 @@ 'merchantId' => '', 'callbackUrl' => '', 'description' => 'payment using saman', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'sep' => [ + 'apiGetToken' => 'https://sep.shaparak.ir/onlinepg/onlinepg', + 'apiPaymentUrl' => 'https://sep.shaparak.ir/OnlinePG/OnlinePG', + 'apiVerificationUrl' => 'https://sep.shaparak.ir/verifyTxnRandomSessionkey/ipg/VerifyTransaction', + 'terminalId' => '', + 'callbackUrl' => '', + 'description' => 'Saman Electronic Payment for Saderat & Keshavarzi', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'sepehr' => [ + 'apiGetToken' => 'https://sepehr.shaparak.ir:8081/V1/PeymentApi/GetToken', + 'apiPaymentUrl' => 'https://sepehr.shaparak.ir:8080/Pay', + 'apiVerificationUrl' => 'https://sepehr.shaparak.ir:8081/V1/PeymentApi/Advice', + 'terminalId' => '', + 'callbackUrl' => '', + 'description' => 'payment using sepehr(saderat)', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'walleta' => [ + 'apiPurchaseUrl' => 'https://cpg.walleta.ir/payment/request.json', + 'apiPaymentUrl' => 'https://cpg.walleta.ir/ticket/', + 'apiVerificationUrl' => 'https://cpg.walleta.ir/payment/verify.json', + 'merchantId' => '', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'description' => 'payment using walleta', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'yekpay' => [ 'apiPurchaseUrl' => 'https://gate.yekpay.com/api/payment/server?wsdl', @@ -172,9 +303,9 @@ ], 'zarinpal' => [ /* normal api */ - 'apiPurchaseUrl' => 'https://ir.zarinpal.com/pg/services/WebGate/wsdl', + 'apiPurchaseUrl' => 'https://api.zarinpal.com/pg/v4/payment/request.json', 'apiPaymentUrl' => 'https://www.zarinpal.com/pg/StartPay/', - 'apiVerificationUrl' => 'https://ir.zarinpal.com/pg/services/WebGate/wsdl', + 'apiVerificationUrl' => 'https://api.zarinpal.com/pg/v4/payment/verify.json', /* sandbox api */ 'sandboxApiPurchaseUrl' => 'https://sandbox.zarinpal.com/pg/services/WebGate/wsdl', @@ -190,6 +321,7 @@ 'merchantId' => '', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using zarinpal', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'zibal' => [ /* normal api */ @@ -202,6 +334,115 @@ 'merchantId' => '', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using zibal', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'sepordeh' => [ + 'apiPurchaseUrl' => 'https://sepordeh.com/merchant/invoices/add', + 'apiPaymentUrl' => 'https://sepordeh.com/merchant/invoices/pay/id:', + 'apiDirectPaymentUrl' => 'https://sepordeh.com/merchant/invoices/pay/automatic:true/id:', + 'apiVerificationUrl' => 'https://sepordeh.com/merchant/invoices/verify', + 'mode' => 'normal', // can be normal, direct + 'merchantId' => '', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'description' => 'payment using sepordeh', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'rayanpay' => [ + 'apiPurchaseUrl' => 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat', + 'apiTokenUrl' => 'https://pms.rayanpay.com/api/v1/auth/token/generate', + 'apiPayStart' => 'https://pms.rayanpay.com/api/v1/ipg/payment/start', + 'apiPayVerify' => 'https://pms.rayanpay.com/api/v1/ipg/payment/response/parse', + 'username' => '', + 'client_id' => '', + 'password' => '', + 'callbackUrl' => '', + 'currency' => 'R', //Can be R, T (Rial, Toman) + ], + 'sizpay' => [ + 'apiPurchaseUrl' => 'https://rt.sizpay.ir/KimiaIPGRouteService.asmx?WSDL', + 'apiPaymentUrl' => 'https://rt.sizpay.ir/Route/Payment', + 'apiVerificationUrl' => 'https://rt.sizpay.ir/KimiaIPGRouteService.asmx?WSDL', + 'merchantId' => '', + 'terminal' => '', + 'username' => '', + 'password' => '', + 'SignData' => '', + 'callbackUrl' => '', + 'currency' => 'R', //Can be R, T (Rial, Toman) + ], + 'vandar' => [ + 'apiPurchaseUrl' => 'https://ipg.vandar.io/api/v3/send', + 'apiPaymentUrl' => 'https://ipg.vandar.io/v3/', + 'apiVerificationUrl' => 'https://ipg.vandar.io/api/v3/verify', + 'callbackUrl' => '', + 'merchantId' => '', + 'description' => 'payment using Vandar', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'aqayepardakht' => [ + 'apiPurchaseUrl' => 'https://panel.aqayepardakht.ir/api/v2/create', + 'apiPaymentUrl' => 'https://panel.aqayepardakht.ir/startpay/', + 'apiPaymentUrlSandbox' => 'https://panel.aqayepardakht.ir/startpay/sandbox/', + 'apiVerificationUrl' => 'https://panel.aqayepardakht.ir/api/v2/verify', + 'mode' => 'normal', //normal | sandbox + 'callbackUrl' => '', + 'pin' => '', + 'invoice_id' => '', + 'mobile' => '', + 'email' => '', + 'description' => 'payment using Aqayepardakht', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'azki' => [ + 'apiPaymentUrl' => 'https://api.azkivam.com', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'fallbackUrl' => 'http://yoursite.com/path/to', + 'merchantId' => '', + 'key' => '', + 'currency' => 'T', //Can be R, T (Rial, Toman) + 'description' => 'payment using azki', + ], + 'payfa' => [ + 'apiPurchaseUrl' => 'https://payment.payfa.com/v2/api/Transaction/Request', + 'apiPaymentUrl' => 'https://payment.payfa.ir/v2/api/Transaction/Pay/', + 'apiVerificationUrl' => 'https://payment.payfa.com/v2/api/Transaction/Verify/', + 'callbackUrl' => '', + 'apiKey' => '', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'toman' => [ + 'base_url' => 'https://escrow-api.toman.ir/api/v1', + 'shop_slug' => '', + 'auth_code' => '', + 'data' => '' + ], + 'bitpay' => [ + 'apiPurchaseUrl' => 'https://bitpay.ir/payment/gateway-send', + 'apiPaymentUrl' => 'https://bitpay.ir/payment/gateway-{id_get}-get', + 'apiVerificationUrl' => 'https://bitpay.ir/payment/gateway-result-second', + 'callbackUrl' => '', + 'api_token' => '', + 'description' => 'payment using Bitpay', + 'currency' => 'R', //Can be R, T (Rial, Toman) + ], + 'minipay' => [ + 'apiPurchaseUrl' => 'https://v1.minipay.me/api/pg/request/', + 'apiPaymentUrl' => 'https://ipg.minipay.me/', + 'apiVerificationUrl' => 'https://v1.minipay.me/api/pg/verify/', + 'merchantId' => '', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'description' => 'payment using Minipay.', + 'currency' => 'T', //Can be R, T (Rial, Toman) + ], + 'snapppay' => [ + 'apiPaymentUrl' => 'https://fms-gateway-staging.apps.public.teh-1.snappcloud.io', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'username' => 'username', + 'password' => 'password', + 'client_id' => '', + 'client_secret' => '', + 'description' => 'payment using Snapp Pay.', + 'currency' => 'T', //Can be R, T (Rial, Toman) ], ], @@ -218,11 +459,19 @@ | */ 'map' => [ + 'local' => \Shetabit\Multipay\Drivers\Local\Local::class, + 'gooyapay' => \Shetabit\Multipay\Drivers\Gooyapay\Gooyapay::class, + 'fanavacard' => \Shetabit\Multipay\Drivers\Fanavacard\Fanavacard::class, 'asanpardakht' => \Shetabit\Multipay\Drivers\Asanpardakht\Asanpardakht::class, + 'atipay' => \Shetabit\Multipay\Drivers\Atipay\Atipay::class, 'behpardakht' => \Shetabit\Multipay\Drivers\Behpardakht\Behpardakht::class, + 'digipay' => \Shetabit\Multipay\Drivers\Digipay\Digipay::class, + 'etebarino' => \Shetabit\Multipay\Drivers\Etebarino\Etebarino::class, 'idpay' => \Shetabit\Multipay\Drivers\Idpay\Idpay::class, 'irankish' => \Shetabit\Multipay\Drivers\Irankish\Irankish::class, + 'jibit' => \Shetabit\Multipay\Drivers\Jibit\Jibit::class, 'nextpay' => \Shetabit\Multipay\Drivers\Nextpay\Nextpay::class, + 'omidpay' => \Shetabit\Multipay\Drivers\Omidpay\Omidpay::class, 'parsian' => \Shetabit\Multipay\Drivers\Parsian\Parsian::class, 'pasargad' => \Shetabit\Multipay\Drivers\Pasargad\Pasargad::class, 'payir' => \Shetabit\Multipay\Drivers\Payir\Payir::class, @@ -232,8 +481,22 @@ 'poolam' => \Shetabit\Multipay\Drivers\Poolam\Poolam::class, 'sadad' => \Shetabit\Multipay\Drivers\Sadad\Sadad::class, 'saman' => \Shetabit\Multipay\Drivers\Saman\Saman::class, + 'sep' => \Shetabit\Multipay\Drivers\SEP\SEP::class, + 'sepehr' => \Shetabit\Multipay\Drivers\Sepehr\Sepehr::class, + 'walleta' => \Shetabit\Multipay\Drivers\Walleta\Walleta::class, 'yekpay' => \Shetabit\Multipay\Drivers\Yekpay\Yekpay::class, 'zarinpal' => \Shetabit\Multipay\Drivers\Zarinpal\Zarinpal::class, 'zibal' => \Shetabit\Multipay\Drivers\Zibal\Zibal::class, + 'sepordeh' => \Shetabit\Multipay\Drivers\Sepordeh\Sepordeh::class, + 'rayanpay' => \Shetabit\Multipay\Drivers\Rayanpay\Rayanpay::class, + 'sizpay' => \Shetabit\Multipay\Drivers\Sizpay\Sizpay::class, + 'vandar' => \Shetabit\Multipay\Drivers\Vandar\Vandar::class, + 'aqayepardakht' => \Shetabit\Multipay\Drivers\Aqayepardakht\Aqayepardakht::class, + 'azki' => \Shetabit\Multipay\Drivers\Azki\Azki::class, + 'payfa' => \Shetabit\Multipay\Drivers\Payfa\Payfa::class, + 'toman' => \Shetabit\Multipay\Drivers\Toman\Toman::class, + 'bitpay' => \Shetabit\Multipay\Drivers\Bitpay\Bitpay::class, + 'minipay' => \Shetabit\Multipay\Drivers\Minipay\Minipay::class, + 'snapppay'=> \Shetabit\Multipay\Drivers\SnappPay\SnappPay::class, ] ]; diff --git a/phpunit.xml b/phpunit.xml index db3e54a..d0d4f3b 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,18 +1,16 @@ - - - - ./src/ - - + stopOnFailure="false" + > ./tests/ diff --git a/resources/images/local-form-fa.png b/resources/images/local-form-fa.png new file mode 100644 index 0000000..14899f6 Binary files /dev/null and b/resources/images/local-form-fa.png differ diff --git a/resources/images/local-form.png b/resources/images/local-form.png new file mode 100644 index 0000000..69dd026 Binary files /dev/null and b/resources/images/local-form.png differ diff --git a/resources/images/payment.png b/resources/images/payment.png index a16aca9..9e3c144 100644 Binary files a/resources/images/payment.png and b/resources/images/payment.png differ diff --git a/resources/views/local-form.php b/resources/views/local-form.php new file mode 100644 index 0000000..5ef6941 --- /dev/null +++ b/resources/views/local-form.php @@ -0,0 +1,118 @@ + + + + + + <?php echo(isset($inputs['title']) ? htmlentities($inputs['title']) : 'درگاه پرداخت تست') ?> + + + + +
+
+
+

+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + diff --git a/resources/views/redirectForm.blade.php b/resources/views/redirect-form.php similarity index 89% rename from resources/views/redirectForm.blade.php rename to resources/views/redirect-form.php index 2828f32..893760f 100644 --- a/resources/views/redirectForm.blade.php +++ b/resources/views/redirect-form.php @@ -60,7 +60,7 @@
-
+

Forwarding to secure payment provider.

If you are not automatically redirected to the payment website with in @@ -68,9 +68,9 @@ seconds...

- @foreach($inputs as $name => $value) - - @endforeach + $value): ?> + +
diff --git a/src/Abstracts/Receipt.php b/src/Abstracts/Receipt.php index 44ef1bf..23030d2 100644 --- a/src/Abstracts/Receipt.php +++ b/src/Abstracts/Receipt.php @@ -65,7 +65,7 @@ public function getReferenceId() : string /** * Retrieve payment date * - * @return Carbon|\Illuminate\Support\Carbon + * @return Carbon */ public function getDate() : Carbon { diff --git a/src/Contracts/ReceiptInterface.php b/src/Contracts/ReceiptInterface.php index 49b3ff8..64c0314 100644 --- a/src/Contracts/ReceiptInterface.php +++ b/src/Contracts/ReceiptInterface.php @@ -23,7 +23,7 @@ public function getReferenceId() : string; /** * Retrieve payment date * - * @return Carbon|\Illuminate\Support\Carbon + * @return Carbon */ public function getDate() : Carbon; } diff --git a/src/Drivers/Aqayepardakht/Aqayepardakht.php b/src/Drivers/Aqayepardakht/Aqayepardakht.php new file mode 100644 index 0000000..aca1e0b --- /dev/null +++ b/src/Drivers/Aqayepardakht/Aqayepardakht.php @@ -0,0 +1,196 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + } + + /** + * @return string + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Shetabit\Multipay\Exceptions\PurchaseFailedException + */ + public function purchase() + { + $data = [ + 'pin' => $this->settings->mode === "normal" ? $this->settings->pin : "sandbox", + 'amount' => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman + 'callback' => $this->settings->callbackUrl, + 'invoice_id' => $this->settings->invoice_id, + 'mobile' => $this->settings->mobile, + 'email' => $this->settings->email, + ]; + + $response = $this->client + ->request( + 'POST', + $this->settings->apiPurchaseUrl, + [ + 'json' => $data, + 'headers' => [ + "Accept" => "application/json", + ], + 'http_errors' => false, + ] + ); + + $responseBody = json_decode($response->getBody()->getContents(), true); + + if ($responseBody['status'] !== self::PAYMENT_STATUS_OK) { + throw new PurchaseFailedException($this->getErrorMessage($responseBody['code'])); + } + + $this->invoice->transactionId($responseBody['transid']); + + return $this->invoice->getTransactionId(); + } + + /** + * @return \Shetabit\Multipay\RedirectionForm + */ + public function pay(): RedirectionForm + { + $url = $this->settings->mode === "normal" ? $this->settings->apiPaymentUrl : $this->settings->apiPaymentUrlSandbox; + $url = $url . $this->invoice->getTransactionId(); + + return $this->redirectWithForm($url, [], 'GET'); + } + + /** + * @return ReceiptInterface + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Shetabit\Multipay\Exceptions\InvalidPaymentException + */ + public function verify(): ReceiptInterface + { + $tracking_number = Request::post("tracking_number"); + $transid = Request::post("transid"); + if ($tracking_number === null || $tracking_number === ""|| $transid === ""|| $transid === null) { + $this->notVerified('پرداخت ناموفق.'); + } + $data = [ + 'pin' => $this->settings->pin, + 'amount' => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman + 'transid' => $transid + ]; + $response = $this->client + ->request( + 'POST', + $this->settings->apiVerificationUrl, + [ + 'json' => $data, + 'headers' => [ + "Accept" => "application/json", + ], + 'http_errors' => false, + ] + ); + + $responseBody = json_decode($response->getBody()->getContents(), true); + + if ($responseBody['status'] !== self::PAYMENT_STATUS_OK) { + if (isset($responseBody['code'])) { + $message = $this->getErrorMessage($responseBody["code"]); + } + + $this->notVerified($message ?? '', $responseBody["code"]); + } + return $this->createReceipt($tracking_number); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId) + { + $receipt = new Receipt('aqayepardakht', $referenceId); + + return $receipt; + } + + /** + * @param $message + * @throws \Shetabit\Multipay\Exceptions\InvalidPaymentException + */ + protected function notVerified($message, $status = 0) + { + if (empty($message)) { + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); + } else { + throw new InvalidPaymentException($message, (int)$status); + } + } + + /** + * @param $code + * @return string + */ + protected function getErrorMessage($code) + { + $code = (int)$code; + switch ($code) { + case -1: return "مبلغ نباید خالی باشد."; + case -2: return "کد پین درگاه نمیتواند خالی باشد."; + case -3: return "آدرس بازگشت نمیتواند خالی باشد."; + case -4: return "مبلغ وارد شده اشتباه است."; + case -5: return "مبلع باید بین 100 تومان تا 50 میلیون تومان باشد."; + case -6: return "کد پین وارد شده اشتباه است."; + case -7: return "کد تراکنش نمیتواند خالی باشد."; + case -8: return "تراکنش مورد نظر وجود ندارد."; + case -9: return "کد پین درگاه با درگاه تراکنش مطابقت ندارد."; + case -10: return "مبلغ با مبلغ تراکنش مطابقت ندارد."; + case -11: return "درگاه در انتظار تایید و یا غیرفعال است."; + case -12: return "امکان ارسال درخواست برای این پذیرنده وجود ندارد."; + case -13: return "شماره کارت باید 16 رقم چسبیده بهم باشد."; + case 0: return "پرداخت انجام نشد."; + case 1: return "پرداخت با موفقیت انجام شد."; + case 2: return "تراکنش قبلا وریفای شده است."; + default: return "خطای نامشخص."; + } + } +} diff --git a/src/Drivers/Asanpardakht/Asanpardakht.php b/src/Drivers/Asanpardakht/Asanpardakht.php index 48ecd31..ab71e05 100644 --- a/src/Drivers/Asanpardakht/Asanpardakht.php +++ b/src/Drivers/Asanpardakht/Asanpardakht.php @@ -2,17 +2,25 @@ namespace Shetabit\Multipay\Drivers\Asanpardakht; +use GuzzleHttp\Client; use Shetabit\Multipay\Abstracts\Driver; -use Shetabit\Multipay\Exceptions\InvalidPaymentException; use Shetabit\Multipay\Exceptions\PurchaseFailedException; use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Invoice; use Shetabit\Multipay\Receipt; use Shetabit\Multipay\RedirectionForm; -use Shetabit\Multipay\Request; class Asanpardakht extends Driver { + const TokenURL = 'Token'; + const TimeURL = 'Time'; + const TranResultURL = 'TranResult'; + const CardHashURL = 'CardHash'; + const SettlementURL = 'Settlement'; + const VerifyURL = 'Verify'; + const CancelURL = 'Cancel'; + const ReverseURL = 'Reverse'; + /** * Invoice * @@ -20,6 +28,19 @@ class Asanpardakht extends Driver */ protected $invoice; + /** + * Response + * + * @var object + */ + protected $response; + + /** + * PayGateTransactionId + * + */ + protected $payGateTransactionId; + /** * Driver settings * @@ -37,7 +58,7 @@ class Asanpardakht extends Driver public function __construct(Invoice $invoice, $settings) { $this->invoice($invoice); - $this->settings = (object) $settings; + $this->settings = (object)$settings; } /** @@ -46,25 +67,18 @@ public function __construct(Invoice $invoice, $settings) * @return string * * @throws PurchaseFailedException - * @throws \SoapFault */ public function purchase() { - $client = $this->createSoapClient($this->settings->apiPurchaseUrl); + $this->invoice->uuid(crc32($this->invoice->getUuid())); - $params = $this->preparePurchaseData(); - $result = $client->RequestOperation($params); - if (!$result) { - throw new PurchaseFailedException('خطای فراخوانی متد درخواست تراکنش.'); - } + $result = $this->token(); - $result = $result->RequestOperationResult; - if ($result{0} != '0') { - $message = "خطای شماره ".$result." رخ داده است."; - throw new PurchaseFailedException($message); + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['status_code']); } - $this->invoice->transactionId(substr($result, 2)); + $this->invoice->transactionId($result['content']); // return the transaction's id return $this->invoice->getTransactionId(); @@ -75,15 +89,18 @@ public function purchase() * * @return RedirectionForm */ - public function pay() : RedirectionForm + public function pay(): RedirectionForm { - $payUrl = $this->settings->apiPaymentUrl; + $data = [ + 'RefID' => $this->invoice->getTransactionId() + ]; + + //set mobileap for get user cards + if (!empty($this->invoice->getDetails()['mobile'])) { + $data['mobileap'] = $this->invoice->getDetails()['mobile']; + } - return $this->redirectWithForm( - $payUrl, - ['RefId' => $this->invoice->getTransactionId()], - 'POST' - ); + return $this->redirectWithForm($this->settings->apiPaymentUrl, $data, 'POST'); } /** @@ -91,87 +108,65 @@ public function pay() : RedirectionForm * * @return mixed|Receipt * - * @throws InvalidPaymentException - * @throws \SoapFault + * @throws PurchaseFailedException */ - public function verify() : ReceiptInterface + public function verify(): ReceiptInterface { - $encryptedReturningParamsString = Request::input('ReturningParams'); - $returningParamsString = decrypt($encryptedReturningParamsString); - $returningParams = explode(",", $returningParamsString); - - /** - * other data: - * $amount = $returningParams[0]; - * $saleOrderId = $returningParams[1]; - * $refId = $this->invoice->getTransactionId() ?? $returningParams[2]; - * $resMessage = $returningParams[4]; - * $rrn = $returningParams[6]; - * $lastFourDigitOfPAN = $returningParams[7]; - **/ - - $resCode = $returningParams[3]; - $payGateTranID = $returningParams[5]; - - if ($resCode != '0' && $resCode != '00') { - $message = "خطای شماره ".$resCode." رخ داده و تراکنش ناموفق بوده است."; - throw new InvalidPaymentException($message); + $result = $this->transactionResult(); + + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['status_code']); } - $client = $this->createSoapClient($this->settings->apiVerificationUrl); - $params = $this->prepareVerificationData($payGateTranID); + $this->payGateTransactionId = $result['content']['payGateTranID']; - // step1: verify - $this->verifyStep($client, $params); + //step1: verify + $verify_result = $this->verifyTransaction(); - // step2: settle - $this->settleStep($client, $params); + if (!isset($verify_result['status_code']) or $verify_result['status_code'] != 200) { + $this->purchaseFailed($verify_result['status_code']); + } - return $this->createReceipt($payGateTranID); - } + //step2: settlement + $this->settlement(); - /** - * payment verification step - * - * @param $client - * @param $params - * - * @throws InvalidPaymentException - */ - protected function verifyStep($client, $params) - { - $result = $client->RequestVerification($params); - if (!$result) { - throw new InvalidPaymentException("خطای فراخوانی متد وريفای رخ داده است."); - } + $receipt = $this->createReceipt($this->payGateTransactionId); + $receipt->detail([ + 'traceNo' => $this->payGateTransactionId, + 'referenceNo' => $result['content']['rrn'], + 'transactionId' => $result['content']['refID'], + 'cardNo' => $result['content']['cardNumber'], + ]); - $result = $result->RequestVerificationResult; - if ($result != '500') { - $message = "خطای شماره: ".$result." در هنگام Verify"; - throw new InvalidPaymentException($message); - } + return $receipt; } /** - * payment settlement step. + * send request to Asanpardakht * - * @param $client - * @param $params - * - * @throws InvalidPaymentException + * @param $method + * @param $url + * @param array $data + * @return array */ - protected function settleStep($client, $params) + protected function callApi($method, $url, $data = []): array { - $result = $client->RequestReconciliation($params); - if (!$result) { - throw new InvalidPaymentException('خطای فراخوانی متد تسويه رخ داده است.'); - } - - $result = $result->RequestReconciliationResult; - if ($result != '600') { - $message = "خطای شماره: ".$result." در هنگام Settlement"; - throw new InvalidPaymentException($message); - } + $client = new Client(['base_uri' => $this->settings->apiRestPaymentUrl]); + + $response = $client->request($method, $url, [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + 'usr' => $this->settings->username, + 'pwd' => $this->settings->password + ], + "http_errors" => false, + ]); + + return [ + 'status_code' => $response->getStatusCode(), + 'content' => json_decode($response->getBody()->getContents(), true) + ]; } /** @@ -181,7 +176,7 @@ protected function settleStep($client, $params) * * @return Receipt */ - protected function createReceipt($referenceId) + protected function createReceipt($referenceId): Receipt { $receipt = new Receipt('asanpardakht', $referenceId); @@ -189,134 +184,167 @@ protected function createReceipt($referenceId) } /** - * Prepare data for payment verification - * - * @param $payGateTranID + * call create token request * * @return array - * - * @throws \SoapFault */ - protected function prepareVerificationData($payGateTranID) + public function token(): array { - $credentials = array( - $this->settings->username, - $this->settings->password - ); - - $encryptedCredentials = $this->encrypt(implode(',', $credentials)); - - return array( - 'merchantConfigurationID' => $this->settings->merchantId, - 'encryptedCredentials' => $encryptedCredentials, - 'payGateTranID' => $payGateTranID - ); + if (strpos($this->settings->callbackUrl, '?') !== false) { + $query = '&' . http_build_query(['invoice' => $this->invoice->getUuid()]); + } else { + $query = '?' . http_build_query(['invoice' => $this->invoice->getUuid()]); + } + + return $this->callApi('POST', self::TokenURL, [ + 'serviceTypeId' => 1, + 'merchantConfigurationId' => $this->settings->merchantConfigID, + 'localInvoiceId' => $this->invoice->getUuid(), + 'amountInRials' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'localDate' => $this->getTime()['content'], + 'callbackURL' => $this->settings->callbackUrl . $query, + 'paymentId' => "0", + 'additionalData' => '', + ]); } /** - * Prepare data for purchasing invoice + * call reserve request * * @return array - * - * @throws \SoapFault */ - protected function preparePurchaseData() + public function reverse(): array { - if (!empty($this->invoice->getDetails()['description'])) { - $description = $this->invoice->getDetails()['description']; - } else { - $description = $this->settings->description; - } - - // configs - $username = $this->settings->username; - $password = $this->settings->password; - $callBackUrl = $this->settings->callbackUrl; - - // invoice details - $price = $this->invoice->getAmount() * 10; // convert to rial - $additionalData = $description ?? ''; - $orderId = crc32($this->invoice->getUuid()); - $localDate = date("Ymd His"); - - // box and encrypt everything - $requestString = "1,{$username},{$password},{$orderId},{$price},{$localDate},{$additionalData},{$callBackUrl},0"; - $encryptedRequestString = $this->encrypt($requestString); - - return array( - 'merchantConfigurationID' => $this->settings->merchantId, - 'encryptedRequest' => $encryptedRequestString - ); + return $this->callApi('POST', self::ReverseURL, [ + 'merchantConfigurationId' => (int)$this->settings->merchantConfigID, + 'payGateTranId' => (int)$this->invoice->getUuid() + ]); } /** - * Encrypt given string. + * send cancel request * - * @param $string - * - * @return mixed - * - * @throws \SoapFault + * @return array */ - protected function encrypt($string) + public function cancel(): array { - $client = $this->createSoapClient($this->settings->apiUtilsUrl); - - $params = array( - 'aesKey' => $this->settings->key, - 'aesVector' => $this->settings->iv, - 'toBeEncrypted' => $string - ); - - $result = $client->EncryptInAES($params); - - return $result->EncryptInAESResult; + return $this->callApi('POST', self::CancelURL, [ + 'merchantConfigurationId' => (int)$this->settings->merchantConfigID, + 'payGateTranId' => (int)$this->payGateTransactionId + ]); } /** - * Decrypt given string. - * - * @param $string - * - * @return mixed + * send verify request * - * @throws \SoapFault + * @return array */ - protected function decrypt($string) + public function verifyTransaction(): array { - $client = $this->createSoapClient($this->settings->apiUtilsUrl); + return $this->callApi('POST', self::VerifyURL, [ + 'merchantConfigurationId' => (int)$this->settings->merchantConfigID, + 'payGateTranId' => (int)$this->payGateTransactionId + ]); + } - $params = array( - 'aesKey' => $this->settings->key, - 'aesVector' => $this->settings->iv, - 'toBeDecrypted' => $string - ); + /** + * send settlement request + * + * @return array + */ + public function settlement(): array + { + return $this->callApi('POST', self::SettlementURL, [ + 'merchantConfigurationId' => (int)$this->settings->merchantConfigID, + 'payGateTranId' => (int)$this->payGateTransactionId + ]); + } - $result = $client->DecryptInAES($params); + /** + * get card hash request + * + * @return array + */ + public function cardHash(): array + { + return $this->callApi('GET', self::CardHashURL . '?merchantConfigurationId=' . $this->settings->merchantConfigID . '&localInvoiceId=' . $this->invoice->getTransactionId(), []); + } - return $result->DecryptInAESResult; + /** + * get transaction result + * + * @return array + */ + public function transactionResult(): array + { + return $this->callApi('GET', self::TranResultURL . '?merchantConfigurationId=' . $this->settings->merchantConfigID . '&localInvoiceId=' . $this->invoice->getTransactionId(), []); } /** - * create a new SoapClient + * get Asanpardakht server time * - * @param $url + * @return array + */ + public function getTime(): array + { + return $this->callApi('GET', self::TimeURL); + } + + /** + * Trigger an exception * - * @return \SoapClient + * @param $status * - * @throws \SoapFault + * @throws PurchaseFailedException */ - protected function createSoapClient($url) + protected function purchaseFailed($status) { - $opts = array( - 'ssl' => array( - 'verify_peer'=>false, - 'verify_peer_name'=>false - ) - ); - - $configs = array('stream_context' => stream_context_create($opts)); - - return new \SoapClient($url, $configs); + $translations = [ + 400 => "bad request", + 401 => "unauthorized. probably wrong or unsent header(s)", + 471 => "identity not trusted to proceed", + 472 => "no records found", + 473 => "invalid merchant username or password", + 474 => "invalid incoming request machine ip. check response body to see your actual public IP address", + 475 => "invoice identifier is not a number", + 476 => "request amount is not a number", + 477 => "request local date length is invalid", + 478 => "request local date is not in valid format", + 479 => "invalid service type id", + 480 => "invalid payer id", + 481 => "incorrect settlement description format", + 482 => "settlement slices does not match total amount", + 483 => "unregistered iban", + 484 => "internal error for other reasons", + 485 => "invalid local date", + 486 => "amount not in range", + 487 => "service not found or not available for merchant", + 488 => "invalid default callback", + 489 => "duplicate local invoice id", + 490 => "merchant disabled or misconfigured", + 491 => "too many settlement destinations", + 492 => "unprocessable request", + 493 => "error processing special request for other reasons like business restrictions", + 494 => "invalid payment_id for governmental payment", + 495 => "invalid referenceId in additionalData", + 496 => "invalid json in additionalData", + 497 => "invalid payment_id location", + 571 => "misconfiguration OR not yet processed", + 572 => "misconfiguration OR transaction status undetermined", + 573 => "misconfiguraed valid ips for configuration OR unable to request for verification due to an internal error", + 574 => "internal error in uthorization", + 575 => "no valid ibans found for merchant", + 576 => "internal error", + 577 => "internal error", + 578 => "no default sharing is defined for merchant", + 579 => "cant submit ibans with default sharing endpoint", + 580 => "error processing special request" + ]; + + if (array_key_exists($status, $translations)) { + throw new PurchaseFailedException($translations[$status]); + } else { + throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); + } } } diff --git a/src/Drivers/Atipay/Atipay.php b/src/Drivers/Atipay/Atipay.php new file mode 100644 index 0000000..b8fa29f --- /dev/null +++ b/src/Drivers/Atipay/Atipay.php @@ -0,0 +1,172 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + $this->tokenId = null; + } + + /** + * Retrieve data from details using its name. + * + * @return string + */ + private function extractDetails($name) + { + return empty($this->invoice->getDetails()[$name]) ? null : $this->invoice->getDetails()[$name]; + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function purchase() + { + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + + $order_id = $this->invoice->getUuid(); + $mobile = $this->extractDetails('mobile'); + $description = $this->extractDetails('description'); + $apikey = $this->settings->apikey; + $redirectUrl = $this->settings->callbackUrl; + + $token_params = array('apiKey'=>$apikey, + 'redirectUrl'=>$redirectUrl, + 'invoiceNumber'=>$order_id, + 'amount'=>$amount, + 'cellNumber'=>$mobile, + ); + + $r = fn_atipay_get_token($token_params); + if ($r['success'] == 1) { + $token = $r['token']; + $this->tokenId = $token; + $this->invoice->transactionId($order_id); + return $this->invoice->getTransactionId(); + } else { + $error_message = $r['errorMessage']; + throw new PurchaseFailedException($error_message); + } + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + //$token = $this->invoice->getTransactionId(); + $token = $this->tokenId; + $payUrl = $this->settings->atipayRedirectGatewayUrl; + return $this->redirectWithForm($payUrl, ['token'=>$token], 'POST'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verify(): ReceiptInterface + { + $params = $_POST; + $result = fn_check_callback_data($params); + $payment_id = $params['reservationNumber']; + if ($result['success'] == 1) { //will verify here + $apiKey = $this->settings->apikey; + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + $verify_params = array('apiKey' => $apiKey, + 'referenceNumber' => $params['referenceNumber'] + ); + + $r = fn_atipay_verify_payment($verify_params, $amount); + if ($r['success'] == 0) { //veriy failed + $error_message = $r['errorMessage']; + throw new InvalidPaymentException($error_message); + } else { //success + $receipt = $this->createReceipt($params['referenceNumber']); + $receipt->detail([ + 'referenceNo' => $params['referenceNumber'], + 'rrn' => Request::input('rrn'), + 'pan' => $params['maskedPan'] + ]); + + return $receipt; + } + } else { + $error_message = $result['error']; + throw new InvalidPaymentException($error_message, (int)$result['success']); + } + + + return $this->createReceipt($body['transId']); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId) + { + $receipt = new Receipt('Atipay', $referenceId); + + return $receipt; + } +} diff --git a/src/Drivers/Atipay/Core/fn.atipay.php b/src/Drivers/Atipay/Core/fn.atipay.php new file mode 100644 index 0000000..50f3600 --- /dev/null +++ b/src/Drivers/Atipay/Core/fn.atipay.php @@ -0,0 +1,224 @@ +'; + $form .= ''; + $form .= ""; + $form .= ''; + + return $form; +} + +function fn_atipay_get_token_form($params, $submit_text, $action) +{ + $form = _fn_generate_get_token_form($params, $submit_text, $action); + return $form; +} + +function _fn_generate_get_token_form($params, $submit_text, $action) +{ + $form ="
"; + foreach ($params as $k=>$v) { + $form .= ""; + } + + $form .= ""; + $form .= "
"; + + return $form; +} + +function fn_check_callback_data($params) +{ + $result = array(); + if (isset($params['state']) && !empty($params['state'])) { + $state = $params['state']; + if ($state == 'OK') { + $result['success']=1; + $result['error']=""; + } else { + $result['success']=0; + $result['error']= _fn_return_state_text($state); + } + } else { + $result['success']=0; + $result['error']="خطای نامشخص در پرداخت. در صورتیکه مبلغی از شما کسر شده باشد، برگشت داده می شود."; + } + + return $result; +} + +function fn_atipay_verify_payment($params, $amount) +{ + $r = wsRequestPost(ATIPAY_VERIFY_URL, $params); + $return = array(); + if ($r) { + if (isset($r['amount']) && !empty($r['amount'])) { + if ($r['amount'] == $amount) { + $return['success']=1; + $return['errorMessage']=""; + } else { + $return['success']=0; + $return['errorMessage']="خطا در تایید مبلغ پرداخت.در صورتیکه مبلغی از شما کسر شده باشد، برگشت داده می شود."; + } + } else { + $return['success']=0; + $return['errorMessage']="خطا در تایید اطلاعات پرداخت. در صورتیکه مبلغی از شما کسر شده باشد، برگشت داده می شود."; + } + } else { + $return['success']=0; + $return['errorMessage'] = "خطا در تایید نهایی پرداخت. در صورتیکه مبلغی از شما کسر شده باشد، برگشت داده می شود."; + } + + return $return; +} + +function _fn_return_state_text($state) +{ + switch ($state) { + case "CanceledByUser": + return "پرداخت توسط شما لغو شده است."; + break; + case "Failed": + return "پرداخت انجام نشد."; + break; + case "SessionIsNull": + return "کاربر در بازه زمانی تعیین شده پاسخی ارسال نکرده است"; + break; + case "InvalidParameters": + return "پارامترهاي ارسالی نامعتبر است"; + break; + case "MerchantIpAddressIsInvalid": + return "آدرس سرور پذیرنده نامعتبر است"; + break; + case "TokenNotFound": + return "توکن ارسال شده یافت نشد"; + break; + case "TokenRequired": + return "با این شماره ترمینال فقط تراکنش هاي توکنی قابل پرداخت هستند"; + break; + case "TerminalNotFound": + return "شماره ترمینال ارسال شده یافت نشد"; + break; + default: + return "خطای نامشخص در عملیات پرداخت"; + } +} + + +function fn_check_callback_params($params) +{ + if (!isset($params['state']) || + !isset($params['status']) || + !isset($params['reservationNumber']) || + !isset($params['referenceNumber']) || + !isset($params['terminalId']) || + !isset($params['traceNumber'])) { + return false; + } else { + return true; + } +} + + + + + +function wsRequestGet($url) +{ + set_time_limit(30); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); //timeout in seconds + $json = curl_exec($ch); + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpcode == "200") { + //nothing YET + } else { + $json= json_encode(array('error'=>'Y')); + } + + return $json; +} + +function wsRequestPost($url, $params) +{ + set_time_limit(30); + $ch = curl_init($url); + $postFields = json_encode($params); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); //timeout in seconds + curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: application/json;")); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); + $json = curl_exec($ch); + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpcode == "200") { + return json_decode($json, true); + } else { + $json = array('error'=>'Y','jsonError'=>$httpcode,'message'=>$httpcode); + } + + return $json; +} + + +function fn_atipay_get_invoice($invoice_id) +{ + $command = 'GetInvoice'; + $postData = array( + 'invoiceid' => $invoice_id, + ); + $results = localAPI($command, $postData); + return $results; +} diff --git a/src/Drivers/Azki/Azki.php b/src/Drivers/Azki/Azki.php new file mode 100644 index 0000000..9aeb381 --- /dev/null +++ b/src/Drivers/Azki/Azki.php @@ -0,0 +1,342 @@ + '/payment/purchase', + 'paymentStatus' => '/payment/status', + 'verify' => '/payment/verify', + ]; + /** + * Azki Client. + * + * @var object + */ + protected $client; + + /** + * Invoice + * + * @var Invoice + */ + protected $invoice; + + /** + * Driver settings + * + * @var object + */ + protected $settings; + + protected $paymentUrl; + + /** + * @return string + */ + public function getPaymentUrl(): string + { + return $this->paymentUrl; + } + + /** + * @param mixed $paymentUrl + */ + public function setPaymentUrl($paymentUrl): void + { + $this->paymentUrl = $paymentUrl; + } + + + public function __construct(Invoice $invoice, $settings) + { + $this->invoice($invoice); + $this->settings = (object)$settings; + $this->client = new Client(); + $this->convertAmountItems(); + } + + public function purchase() + { + $details = $this->invoice->getDetails(); + $order_id = $this->invoice->getUuid(); + + if (empty($details['phone']) && empty($details['mobile'])) { + throw new PurchaseFailedException('Phone number is required'); + } + if (!isset($details['items']) || count($details['items']) == 0) { + throw new PurchaseFailedException('Items is required for this driver'); + } + + $merchant_id = $this->settings->merchantId; + $callback = $this->settings->callbackUrl; + $fallback = + $this->settings->fallbackUrl != 'http://yoursite.com/path/to' && $this->settings->fallbackUrl == '' + ? $this->settings->fallbackUrl + : $callback; + $sub_url = self::subUrls['purchase']; + $url = $this->settings->apiPaymentUrl . $sub_url; + + $signature = $this->makeSignature( + $sub_url, + 'POST' + ); + + $data = [ + "amount" => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + "redirect_uri" => $callback, + "fallback_uri" => $fallback, + "provider_id" => $order_id, + "mobile_number" => $details['mobile'] ?? $details['phone'] ?? null, + "merchant_id" => $merchant_id, + "description" => $details['description'] ?? $this->settings->description, + "items" => $details['items'], + ]; + + $response = $this->ApiCall($data, $signature, $url); + + // set transaction's id + $this->invoice->transactionId($response['ticket_id']); + $this->setPaymentUrl($response['payment_uri']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + public function pay(): RedirectionForm + { + $url = $this->getPaymentUrl(); + return $this->redirectWithForm( + $url, + [ + 'ticketId' => $this->invoice->getTransactionId(), + ], + 'GET' + ); + } + + public function verify(): ReceiptInterface + { + $paymentStatus = $this->getPaymentStatus(); + + if ($paymentStatus != self::STATUS_DONE) { + $this->verifyFailed($paymentStatus); + } + + $this->VerifyTransaction(); + + return $this->createReceipt($this->invoice->getTransactionId()); + } + + + private function makeSignature($sub_url, $request_method = 'POST') + { + $time = time(); + $key = $this->settings->key; + + $plain_signature = "{$sub_url}#{$time}#{$request_method}#{$key}"; + $encrypt_method = "AES-256-CBC"; + $secret_key = hex2bin($key); + $secret_iv = str_repeat(0, 16); + + $digest = @openssl_encrypt($plain_signature, $encrypt_method, $secret_key, OPENSSL_RAW_DATA); + + return bin2hex($digest); + } + + private function convertAmountItems() + { + /** + * example data + * + * $items = [ + * [ + * "name" => "Item 1", + * "count" => "string", + * "amount" => 0, + * "url" => "http://shop.com/items/1", + * ], + * [ + * "name" => "Item 2", + * "count" => 5, + * "amount" => 20000, + * "url" => "http://google.com/items/2", + * ], + * ]; + * + */ + + $new_items = array_map( + function ($item) { + $item['amount'] *= ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + return $item; + }, + $this->invoice->getDetails()['items'] ?? [] + ); + + $this->invoice->detail('items', $new_items); + return $new_items; + } + + /** + * @param array $data + * @param $signature + * @param $url + * @return mixed + */ + public function ApiCall(array $data, $signature, $url, $request_method = 'POST') + { + $response = $this + ->client + ->request( + $request_method, + $url, + [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + 'Signature' => $signature, + 'MerchantId' => $this->settings->merchantId, + ], + "http_errors" => false, + ] + ); + + $response_array = json_decode($response->getBody()->getContents(), true); + + + if (($response->getStatusCode() === null or $response->getStatusCode() != 200) || $response_array['rsCode'] != self::SUCCESSFUL) { + $this->purchaseFailed($response_array['rsCode']); + } else { + return $response_array['result']; + } + } + + /** + * Trigger an exception + * + * @param $status + * + * @throws PurchaseFailedException + */ + protected function purchaseFailed($status) + { + $translations = [ + "1" => "Internal Server Error", + "2" => "Resource Not Found", + "4" => "Malformed Data", + "5" => "Data Not Found", + "15" => "Access Denied", + "16" => "Transaction already reversed", + "17" => "Ticket Expired", + "18" => "Signature Invalid", + "19" => "Ticket unpayable", + "20" => "Ticket customer mismatch", + "21" => "Insufficient Credit", + "28" => "Unverifiable ticket due to status", + "32" => "Invalid Invoice Data", + "33" => "Contract is not started", + "34" => "Contract is expired", + "44" => "Validation exception", + "51" => "Request data is not valid", + "59" => "Transaction not reversible", + "60" => "Transaction must be in verified state", + ]; + + if (array_key_exists($status, $translations)) { + throw new PurchaseFailedException($translations[$status]); + } else { + throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); + } + } + + private function getPaymentStatus() + { + $sub_url = self::subUrls['paymentStatus']; + $url = $this->settings->apiPaymentUrl . $sub_url; + + $signature = $this->makeSignature( + $sub_url, + 'POST' + ); + + $data = [ + "ticket_id" => $this->invoice->getTransactionId(), + ]; + + return $this->ApiCall($data, $signature, $url)['status']; + } + + + /** + * Trigger an exception + * + * @param $status + * + * @throws PurchaseFailedException + */ + protected function verifyFailed($status) + { + $translations = [ + "1" => "Created", + "2" => "Verified", + "3" => "Reversed", + "4" => "Failed", + "5" => "Canceled", + "6" => "Settled", + "7" => "Expired", + "8" => "Done", + "9" => "Settle Queue", + ]; + + if (array_key_exists($status, $translations)) { + throw new PurchaseFailedException("تراکنش در وضعیت " . $translations[$status] . " است."); + } else { + throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); + } + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + private function createReceipt($referenceId): Receipt + { + $receipt = new Receipt('azki', $referenceId); + + return $receipt; + } + + private function VerifyTransaction() + { + $sub_url = self::subUrls['verify']; + $url = $this->settings->apiPaymentUrl . $sub_url; + + $signature = $this->makeSignature( + $sub_url, + 'POST' + ); + + $data = [ + "ticket_id" => $this->invoice->getTransactionId(), + ]; + + return $this->ApiCall($data, $signature, $url); + } +} diff --git a/src/Drivers/Behpardakht/Behpardakht.php b/src/Drivers/Behpardakht/Behpardakht.php index 76e1585..82569c3 100644 --- a/src/Drivers/Behpardakht/Behpardakht.php +++ b/src/Drivers/Behpardakht/Behpardakht.php @@ -11,6 +11,7 @@ use Shetabit\Multipay\Request; use Carbon\Carbon; use Shetabit\Multipay\RedirectionForm; +use SoapClient; class Behpardakht extends Driver { @@ -38,7 +39,7 @@ class Behpardakht extends Driver public function __construct(Invoice $invoice, $settings) { $this->invoice($invoice); - $this->settings = (object) $settings; + $this->settings = (object)$settings; } /** @@ -49,28 +50,33 @@ public function __construct(Invoice $invoice, $settings) * @throws PurchaseFailedException * @throws \SoapFault */ - + public function purchase() { - $soap = new \SoapClient($this->settings->apiPurchaseUrl); - $response = $soap->bpPayRequest($this->preparePurchaseData()); + $soap = $this->client($this->settings->apiPurchaseUrl); + + $purchaseData = $this->preparePurchaseData(); + + if ($this->settings->cumulativeDynamicPayStatus) { + $response = $soap->bpCumulativeDynamicPayRequest($purchaseData); + } else { + $response = $soap->bpPayRequest($purchaseData); + } // fault has happened in bank gateway if ($response->return == 21) { - throw new PurchaseFailedException('پذیرنده معتبر نیست.'); + throw new PurchaseFailedException('پذیرنده معتبر نیست.', 21); } - $data = explode(',', $response->return); // purchase was not successful if ($data[0] != "0") { - throw new PurchaseFailedException($response); + throw new PurchaseFailedException($this->translateStatus($data[0]), (int)$data[0]); } $this->invoice->transactionId($data[1]); - // return the transaction's id return $this->invoice->getTransactionId(); } @@ -79,17 +85,19 @@ public function purchase() * * @return RedirectionForm */ - public function pay() : RedirectionForm + public function pay(): RedirectionForm { $payUrl = $this->settings->apiPaymentUrl; - return $this->redirectWithForm( - $payUrl, - [ - 'RefId' => $this->invoice->getTransactionId(), - ], - 'POST' - ); + $data = [ + 'RefId' => $this->invoice->getTransactionId() + ]; + + if (!empty($this->invoice->getDetails()['mobile'])) { + $data['MobileNo'] = $this->invoice->getDetails()['mobile']; + } + + return $this->redirectWithForm($payUrl, $data, 'POST'); } /** @@ -100,34 +108,52 @@ public function pay() : RedirectionForm * @throws InvalidPaymentException * @throws \SoapFault */ - public function verify() : ReceiptInterface + public function verify(): ReceiptInterface { $resCode = Request::input('ResCode'); if ($resCode != '0') { - $message = $resCode ?? 'تراکنش نا موفق بوده است.'; - throw new InvalidPaymentException($message); + throw new InvalidPaymentException($this->translateStatus($resCode), $resCode); } $data = $this->prepareVerificationData(); - $soap = new \SoapClient($this->settings->apiVerificationUrl); + + $soap = $this->client($this->settings->apiVerificationUrl); // step1: verify request - $verifyResponse = (int)$soap->bpVerifyRequest($data, $this->settings->apiNamespaceUrl)->return; + $verifyResponse = (int)$soap->bpVerifyRequest($data)->return; if ($verifyResponse != 0) { // rollback money and throw exception - $soap->bpReversalRequest($data, $this->settings->apiNamespaceUrl); - throw new InvalidPaymentException($verifyResponse ?? "خطا در عملیات وریفای تراکنش"); + // avoid rollback if request was already verified + if ($verifyResponse != 43) { + $soap->bpReversalRequest($data); + } + + throw new InvalidPaymentException($this->translateStatus($verifyResponse), $verifyResponse); } // step2: settle request - $settleResponse = $soap->bpSettleRequest($data, $this->settings->apiNamespaceUrl)->return; + $settleResponse = $soap->bpSettleRequest($data)->return; if ($settleResponse != 0) { // rollback money and throw exception - $soap->bpReversalRequest($data, $this->settings->apiNamespaceUrl); - throw new InvalidPaymentException($settleResponse ?? "خطا در ثبت درخواست واریز وجه"); + // avoid rollback if request was already settled/reversed + if ($settleResponse != 45 && $settleResponse != 48) { + $soap->bpReversalRequest($data); + } + + throw new InvalidPaymentException($this->translateStatus($settleResponse), $settleResponse); } - return $this->createReceipt($data['saleReferenceId']); + $receipt = $this->createReceipt($data['saleReferenceId']); + + $receipt->detail([ + "RefId" => Request::input('RefId'), + "SaleOrderId" => Request::input('SaleOrderId'), + "CardHolderPan" => Request::input('CardHolderPan'), + "CardHolderInfo" => Request::input('CardHolderInfo'), + "SaleReferenceId" => Request::input('SaleReferenceId'), + ]); + + return $receipt; } /** @@ -139,9 +165,7 @@ public function verify() : ReceiptInterface */ protected function createReceipt($referenceId) { - $receipt = new Receipt('behpardakht', $referenceId); - - return $receipt; + return new Receipt('behpardakht', $referenceId); } /** @@ -156,12 +180,12 @@ protected function prepareVerificationData() $verifySaleReferenceId = Request::input('SaleReferenceId'); return array( - 'terminalId' => $this->settings->terminalId, - 'userName' => $this->settings->username, - 'userPassword' => $this->settings->password, - 'orderId' => $orderId, - 'saleOrderId' => $verifySaleOrderId, - 'saleReferenceId' => $verifySaleReferenceId + 'terminalId' => $this->settings->terminalId, + 'userName' => $this->settings->username, + 'userPassword' => $this->settings->password, + 'orderId' => $orderId, + 'saleOrderId' => $verifySaleOrderId, + 'saleReferenceId' => $verifySaleReferenceId ); } @@ -181,16 +205,90 @@ protected function preparePurchaseData() $payerId = $this->invoice->getDetails()['payerId'] ?? 0; return array( - 'terminalId' => $this->settings->terminalId, - 'userName' => $this->settings->username, - 'userPassword' => $this->settings->password, - 'callBackUrl' => $this->settings->callbackUrl, - 'amount' => $this->invoice->getAmount() * 10, // convert to rial - 'localDate' => Carbon::now()->format('Ymd'), - 'localTime' => Carbon::now()->format('Gis'), - 'orderId' => crc32($this->invoice->getUuid()), - 'additionalData' => $description, - 'payerId' => $payerId + 'terminalId' => $this->settings->terminalId, + 'userName' => $this->settings->username, + 'userPassword' => $this->settings->password, + 'callBackUrl' => $this->settings->callbackUrl, + 'amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'localDate' => Carbon::now()->format('Ymd'), + 'localTime' => Carbon::now()->format('Gis'), + 'orderId' => crc32($this->invoice->getUuid()), + 'additionalData' => $description, + 'payerId' => $payerId ); } + + private function client(string $url): SoapClient + { + if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] == "HTTP/2.0") { + $context = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]); + + return new SoapClient($url, ['stream_context' => $context]); + } + + return new SoapClient($url); + } + + /** + * Convert status to a readable message. + * + * @param $status + * + * @return mixed|string + */ + private function translateStatus($status) + { + $translations = [ + '0' => 'تراکنش با موفقیت انجام شد', + '11' => 'شماره کارت نامعتبر است', + '12' => 'موجودی کافی نیست', + '13' => 'رمز نادرست است', + '14' => 'تعداد دفعات وارد کردن رمز بیش از حد مجاز است', + '15' => 'کارت نامعتبر است', + '16' => 'دفعات برداشت وجه بیش از حد مجاز است', + '17' => 'کاربر از انجام تراکنش منصرف شده است', + '18' => 'تاریخ انقضای کارت گذشته است', + '19' => 'مبلغ برداشت وجه بیش از حد مجاز است', + '111' => 'صادر کننده کارت نامعتبر است', + '112' => 'خطای سوییچ صادر کننده کارت', + '113' => 'پاسخی از صادر کننده کارت دریافت نشد', + '114' => 'دارنده کارت مجاز به انجام این تراکنش نیست', + '21' => 'پذیرنده نامعتبر است', + '23' => 'خطای امنیتی رخ داده است', + '24' => 'اطلاعات کاربری پذیرنده نامعتبر است', + '25' => 'مبلغ نامعتبر است', + '31' => 'پاسخ نامعتبر است', + '32' => 'فرمت اطلاعات وارد شده صحیح نمی‌باشد', + '33' => 'حساب نامعتبر است', + '34' => 'خطای سیستمی', + '35' => 'تاریخ نامعتبر است', + '41' => 'شماره درخواست تکراری است', + '42' => 'تراکنش Sale یافت نشد', + '43' => 'قبلا درخواست Verify داده شده است', + '44' => 'درخواست Verify یافت نشد', + '45' => 'تراکنش Settle شده است', + '46' => 'تراکنش Settle نشده است', + '47' => 'تراکنش Settle یافت نشد', + '48' => 'تراکنش Reverse شده است', + '412' => 'شناسه قبض نادرست است', + '413' => 'شناسه پرداخت نادرست است', + '414' => 'سازمان صادر کننده قبض نامعتبر است', + '415' => 'زمان جلسه کاری به پایان رسیده است', + '416' => 'خطا در ثبت اطلاعات', + '417' => 'شناسه پرداخت کننده نامعتبر است', + '418' => 'اشکال در تعریف اطلاعات مشتری', + '419' => 'تعداد دفعات ورود اطلاعات از حد مجاز گذشته است', + '421' => 'IP نامعتبر است', + '51' => 'تراکنش تکراری است', + '54' => 'تراکنش مرجع موجود نیست', + '55' => 'تراکنش نامعتبر است', + '61' => 'خطا در واریز', + '62' => 'مسیر بازگشت به سایت در دامنه ثبت شده برای پذیرنده قرار ندارد', + '98' => 'سقف استفاده از رمز ایستا به پایان رسیده است' + ]; + + $unknownError = 'خطای ناشناخته رخ داده است.'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } } diff --git a/src/Drivers/Bitpay/Bitpay.php b/src/Drivers/Bitpay/Bitpay.php new file mode 100644 index 0000000..336e5e6 --- /dev/null +++ b/src/Drivers/Bitpay/Bitpay.php @@ -0,0 +1,182 @@ +invoice($invoice); + $this->settings = (object) $settings; + } + + /** + * Retrieve data from details using its name. + * + * @return string + */ + private function extractDetails($name) + { + return empty($this->invoice->getDetails()[$name]) ? null : $this->invoice->getDetails()[$name]; + } + + /** + * @return string + * + * @throws \Shetabit\Multipay\Exceptions\PurchaseFailedException + */ + public function purchase() + { + $name = $this->extractDetails('name'); + $email = $this->extractDetails('email'); + $description = $this->extractDetails('description'); + $factorId = $this->extractDetails('factorId'); + $api = $this->settings->api_token; + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + $redirect = $this->settings->callbackUrl; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->settings->apiPurchaseUrl); + curl_setopt($ch, CURLOPT_POSTFIELDS, "api={$api}&amount={$amount}&redirect={$redirect}&factorId={$factorId}&name={$name}&email={$email}&description={$description}"); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $res = curl_exec($ch); + curl_close($ch); + + if ($res > 0 && is_numeric($res)) { + $this->invoice->transactionId($res); + + return $this->invoice->getTransactionId(); + } + + throw new PurchaseFailedException($this->purchaseTranslateStatus($res), $res); + } + + /** + * @return \Shetabit\Multipay\RedirectionForm + */ + public function pay(): RedirectionForm + { + $url = str_replace('{id_get}', $this->invoice->getTransactionId(), $this->settings->apiPaymentUrl); + + return $this->redirectWithForm($url, [], 'GET'); + } + + /** + * @return ReceiptInterface + * + * @throws \Shetabit\Multipay\Exceptions\InvalidPaymentException + */ + public function verify(): ReceiptInterface + { + $trans_id = Request::get('trans_id'); + $id_get = Request::get('id_get'); + $api = $this->settings->api_token; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->settings->apiVerificationUrl); + curl_setopt($ch, CURLOPT_POSTFIELDS, "api=$api&id_get=$id_get&trans_id=$trans_id&json=1"); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $res = curl_exec($ch); + curl_close($ch); + + $parseDecode = json_decode($res); + $statusCode = $parseDecode->status; + + if ($statusCode != 1) { + throw new InvalidPaymentException($this->paymentTranslateStatus($statusCode), $statusCode); + } + + $receipt = $this->createReceipt($trans_id); + + $receipt->detail([ + "amount" => $parseDecode->amount / ($this->settings->currency == 'T' ? 10 : 1), // convert to config currency + "cardNum" => $parseDecode->cardNum, + "factorId" => $parseDecode->factorId, + ]); + + return $receipt; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId) + { + $receipt = new Receipt('bitpay', $referenceId); + + return $receipt; + } + + /** + * Convert purchase status to a readable message. + * + * @param $status + * + * @return string + */ + private function purchaseTranslateStatus($status) + { + $translations = [ + '-1' => 'Api ارسالی با نوع Api تعریف شده در bitpay سازگار نیست', + '-2' => 'مقدار amount داده عددی نمی‌باشد و یا کمتر از ۱۰۰۰ ریال است', + '-3' => 'مقدار redirect رشته null است', + '-4' => 'درگاهی با اطلاعات ارسالی شما وجود ندارد و یا در حالت انتظار می‌باشد', + '-5' => 'خطا در اتصال به درگاه، لطفا مجدد تلاش کنید', + ]; + + $unknownError = 'خطای ناشناخته رخ داده است. در صورت کسر مبلغ از حساب حداکثر پس از 72 ساعت به حسابتان برمیگردد'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } + + /** + * Convert payment status to a readable message. + * + * @param $status + * + * @return string + */ + private function paymentTranslateStatus($status) + { + $translations = [ + '-1' => 'Api ارسالی با نوع Api تعریف شده در bitpay سازگار نیست', + '-2' => 'tran_id ارسال شده، داده عددی نمی‌باشد', + '-3' => 'id_get ارسال شده، داده عددی نمی‌باشد', + '-4' => 'چنین تراکنشی در پایگاه داده وجود ندارد و یا موفقیت آمیز نبوده است', + '11' => 'تراکنش از قبل وریفای شده است', + ]; + + $unknownError = 'خطای ناشناخته رخ داده است. در صورت کسر مبلغ از حساب حداکثر پس از 72 ساعت به حسابتان برمیگردد'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } +} diff --git a/src/Drivers/Digipay/Digipay.php b/src/Drivers/Digipay/Digipay.php new file mode 100644 index 0000000..2db4746 --- /dev/null +++ b/src/Drivers/Digipay/Digipay.php @@ -0,0 +1,433 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + $this->oauthToken = $this->oauth(); + } + + /** + * @throws PurchaseFailedException + */ + public function purchase(): string + { + $phone = $this->invoice->getDetail('phone') + ?? $this->invoice->getDetail('cellphone') + ?? $this->invoice->getDetail('mobile'); + + /** + * @see https://docs.mydigipay.com/upg.html#_request_fields_2 + */ + $data = [ + 'amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), + 'cellNumber' => $phone, + 'providerId' => $this->invoice->getUuid(), + 'callbackUrl' => $this->settings->callbackUrl, + ]; + + if (!is_null($basketDetailsDto = $this->invoice->getDetail('basketDetailsDto'))) { + $data['basketDetailsDto'] = $basketDetailsDto; + } + + if (!is_null($preferredGateway = $this->invoice->getDetail('preferredGateway'))) { + $data['preferredGateway'] = $preferredGateway; + } + + if (!is_null($splitDetailsList = $this->invoice->getDetail('splitDetailsList'))) { + $data['splitDetailsList'] = $splitDetailsList; + } + + /** + * @see https://docs.mydigipay.com/upg.html#_query_parameters_2 + */ + $digipayType = $this->invoice->getDetail('digipayType') ?? 11; + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPaymentUrl.self::PURCHASE_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::QUERY => ['type' => $digipayType], + RequestOptions::HEADERS => [ + 'Agent' => $this->invoice->getDetail('agent') ?? 'WEB', + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + 'Digipay-Version' => self::VERSION, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200) { + // error has happened + $message = $body['result']['message'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.'; + throw new PurchaseFailedException($message); + } + + $this->invoice->transactionId($body['ticket']); + $this->setPaymentUrl($body['redirectUrl']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + public function pay(): RedirectionForm + { + return $this->redirectWithForm($this->getPaymentUrl(), [], 'GET'); + } + + /** + * @throws InvalidPaymentException + */ + public function verify(): ReceiptInterface + { + /** + * @see https://docs.mydigipay.com/upg.html#ticket_type + */ + $digipayTicketType = Request::input('type'); + $tracingId = Request::input('trackingCode'); + + $response = $this->client->request( + 'POST', + $this->settings->apiPaymentUrl.self::VERIFY_URL.$tracingId, + [ + RequestOptions::QUERY => ['type' => $digipayTicketType], + RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200) { + $message = $body['result']['message'] ?? 'تراکنش تایید نشد'; + throw new InvalidPaymentException($message, (int) $response->getStatusCode()); + } + + return (new Receipt('digipay', $body["trackingCode"]))->detail($body); + } + + /** + * @throws PurchaseFailedException + */ + protected function oauth() + { + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPaymentUrl.self::OAUTH_URL, + [ + RequestOptions::HEADERS => [ + 'Authorization' => 'Basic '.base64_encode("{$this->settings->client_id}:{$this->settings->client_secret}"), + ], + RequestOptions::MULTIPART => [ + [ + 'name' => 'username', + 'contents' => $this->settings->username, + ], + [ + 'name' => 'password', + 'contents' => $this->settings->password, + ], + [ + 'name' => 'grant_type', + 'contents' => 'password', + ], + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + if ($response->getStatusCode() != 200) { + if ($response->getStatusCode() == 401) { + throw new PurchaseFailedException('خطا نام کاربری یا رمز عبور شما اشتباه می‌باشد.'); + } else { + throw new PurchaseFailedException('خطا در هنگام احراز هویت.'); + } + } + + $body = json_decode($response->getBody()->getContents(), true); + + $this->oauthToken = $body['access_token']; + + return $body['access_token']; + } + + /** + * @see https://docs.mydigipay.com/upg.html#_purchase_reverse + * + * @throws PurchaseFailedException + */ + public function reverse() + { + if (is_null($digipayTicketType = $this->invoice->getDetail('type'))) { + throw new PurchaseFailedException('"type" is required for this method.'); + } + + if (is_null($trackingCode = $this->invoice->getDetail('trackingCode'))) { + throw new PurchaseFailedException('"trackingCode" is required for this method.'); + } + + $data = [ + 'trackingCode' => $trackingCode, + 'providerId' => $this->invoice->getTransactionId() ?? $this->invoice->getDetail('providerId') ?? $this->invoice->getUuid(), + ]; + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPaymentUrl.self::REVERSE_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::QUERY => ['type' => $digipayTicketType], + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json;charset=UTF-8', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || (isset($body['result']['code']) && $body['result']['code'] != 0)) { + $message = $body['result']['message'] ?? 'خطا در هنگام درخواست برای برگشت وجه رخ داده است.'; + throw new InvalidPaymentException($message, (int) $response->getStatusCode()); + } + + return $body; + } + + /** + * @see https://docs.mydigipay.com/upg.html#_purchase_delivery + * + * @throws PurchaseFailedException + */ + public function deliver() + { + if (empty($type = $this->invoice->getDetail('type'))) { + throw new PurchaseFailedException('"type" is required for this method.'); + } + + if (!in_array($type, [5, 13])) { + throw new PurchaseFailedException('This method is not supported for this type.'); + } + + if (empty($invoiceNumber = $this->invoice->getDetail('invoiceNumber'))) { + throw new PurchaseFailedException('"invoiceNumber" is required for this method.'); + } + + if (empty($deliveryDate = $this->invoice->getDetail('deliveryDate'))) { + throw new PurchaseFailedException('"deliveryDate" is required for this method.'); + } + + if (!DateTime::createFromFormat('Y-m-d', $deliveryDate)) { + throw new PurchaseFailedException('"deliveryDate" must be a valid date with format Y-m-d.'); + } + + if (empty($trackingCode = $this->invoice->getDetail('trackingCode'))) { + throw new PurchaseFailedException('"trackingCode" is required for this method.'); + } + + if (empty($products = $this->invoice->getDetail('products'))) { + throw new PurchaseFailedException('"products" is required for this method.'); + } + + if (!is_array($products)) { + throw new PurchaseFailedException('"products" must be an array.'); + } + + $data = [ + 'invoiceNumber' => $invoiceNumber, + 'deliveryDate' => $deliveryDate, + 'trackingCode' => $trackingCode, + 'products' => $products, + ]; + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPaymentUrl.self::DELIVER_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::QUERY => ['type' => $type], + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json;charset=UTF-8', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || (isset($body['result']['code']) && $body['result']['code'] != 0)) { + $message = $body['result']['message'] ?? 'خطا در هنگام درخواست برای تحویل کالا رخ داده است.'; + throw new InvalidPaymentException($message, (int) $response->getStatusCode()); + } + + return $body; + } + + public function getRefundConfig() + { + $response = $this->client->request( + 'POST', + $this->settings->apiPaymentUrl.self::REFUNDS_CONFIG, + [ + RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || (isset($body['result']['code']) && $body['result']['code'] != 0)) { + $message = $body['result']['message'] ?? 'خطا در هنگام درخواست برای دریافت تنظیمات مرجوعی رخ داده است.'; + throw new InvalidPaymentException($message, (int) $response->getStatusCode()); + } + + $certFile = $response['certFile']; + + return $certFile; + } + + public function refundTransaction() + { + if (empty($type = $this->invoice->getDetail('type'))) { + throw new PurchaseFailedException('"type" is required for this method.'); + } + + if (empty($providerId = $this->invoice->getDetail('providerId'))) { + throw new PurchaseFailedException('"providerId" is required for this method.'); + } + + if (empty($amount = $this->invoice->getDetail('amount'))) { + throw new PurchaseFailedException('"amount" is required for this method.'); + } + + if (empty($saleTrackingCode = $this->invoice->getDetail('saleTrackingCode'))) { + throw new PurchaseFailedException('"saleTrackingCode" is required for this method.'); + } + + $data = [ + 'providerId' => $providerId, + 'amount' => $amount, + 'saleTrackingCode' => $saleTrackingCode, + ]; + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPaymentUrl.self::REFUNDS_REQUEST, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::QUERY => ['type' => $type], + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json;charset=UTF-8', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || (isset($body['result']['code']) && $body['result']['code'] != 0)) { + $message = $body['result']['message'] ?? 'خطا در هنگام درخواست مرجوعی تراکنش رخ داده است.'; + throw new InvalidPaymentException($message, (int) $response->getStatusCode()); + } + + return $body; + } + + private function getPaymentUrl(): string + { + return $this->paymentUrl; + } + + private function setPaymentUrl(string $paymentUrl): void + { + $this->paymentUrl = $paymentUrl; + } +} diff --git a/src/Drivers/Etebarino/Etebarino.php b/src/Drivers/Etebarino/Etebarino.php new file mode 100644 index 0000000..04c3f12 --- /dev/null +++ b/src/Drivers/Etebarino/Etebarino.php @@ -0,0 +1,209 @@ +invoice($invoice); + $this->settings = (object)$settings; + } + + /** + * Purchase Invoice + * + * @return string + * + * @throws PurchaseFailedException + */ + public function purchase() + { + $this->invoice->uuid(crc32($this->invoice->getUuid())); + + $result = $this->token(); + + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['content']); + } + + $this->invoice->transactionId($result['content']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + return $this->redirectWithForm($this->settings->apiPaymentUrl, [ + 'token' => $this->invoice->getTransactionId() + ], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws PurchaseFailedException + */ + public function verify(): ReceiptInterface + { + $result = $this->verifyTransaction(); + + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['content']); + } + + $receipt = $this->createReceipt($this->invoice->getDetail('referenceCode')); + $receipt->detail([ + 'referenceNo' => $this->invoice->getDetail('referenceCode'), + ]); + + return $receipt; + } + + /** + * send request to Etebarino + * + * @param $method + * @param $url + * @param array $data + * @return array + */ + protected function callApi($method, $url, $data = []): array + { + $client = new Client(); + + $response = $client->request($method, $url, [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ]); + + return [ + 'status_code' => $response->getStatusCode(), + 'content' => $response->getBody()->getContents() + ]; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId): Receipt + { + $receipt = new Receipt('etebarino', $referenceId); + + return $receipt; + } + + /** + * call create token request + * + * @return array + */ + public function token(): array + { + return $this->callApi('POST', $this->settings->apiPurchaseUrl, [ + 'terminalCode' => $this->settings->terminalId, + 'terminalUser' => $this->settings->username, + 'merchantCode' => $this->settings->merchantId, + 'terminalPass' => $this->settings->password, + 'merchantRefCode' => $this->invoice->getUuid(), + "description" => $this->invoice->getDetail('description'), + "returnUrl" => $this->settings->callbackUrl, + 'paymentItems' => $this->getItems(), + ]); + } + + /** + * call verift transaction request + * + * @return array + */ + public function verifyTransaction(): array + { + return $this->callApi('POST', $this->settings->apiVerificationUrl, [ + 'terminalCode' => $this->settings->terminalId, + 'terminalUser' => $this->settings->username, + 'merchantCode' => $this->settings->merchantId, + 'terminalPass' => $this->settings->password, + 'merchantRefCode' => $this->invoice->getDetail('uuid'), + 'referenceCode' => $this->invoice->getDetail('referenceCode') + ]); + } + + /** + * get Items for + * + * + */ + private function getItems() + { + /** + * example data + * + * $items = [ + * [ + * "productGroup" => 1000, + * "amount" => 1000, //Rial + * "description" => "desc" + * ] + * ]; + */ + return $this->invoice->getDetails()['items']; + } + + + /** + * Trigger an exception + * + * @param $message + * + * @throws PurchaseFailedException + */ + protected function purchaseFailed($message) + { + throw new PurchaseFailedException($message); + } +} diff --git a/src/Drivers/Fanavacard/Fanavacard.php b/src/Drivers/Fanavacard/Fanavacard.php new file mode 100644 index 0000000..c0a6cd9 --- /dev/null +++ b/src/Drivers/Fanavacard/Fanavacard.php @@ -0,0 +1,198 @@ +invoice($invoice); + $this->settings = (object)$settings; + $this->httpClientInit(); + } + + /** + * Purchase Invoice + * + * @return string + * + * @throws PurchaseFailedException + */ + public function purchase() + { + $this->invoice->uuid(crc32($this->invoice->getUuid())); + $token = $this->getToken(); + $this->invoice->transactionId($token['Token']); + + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + $url = rtrim($this->settings->baseUri, '/')."/{$this->settings->apiPaymentUrl}"; + + return $this->redirectWithForm($url, [ + 'token' => $this->invoice->getTransactionId(), + 'language' => 'fa' + ], 'POST'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws PurchaseFailedException + * @throws InvalidPaymentException + */ + public function verify(): ReceiptInterface + { + $transaction_amount = Request::input('transactionAmount'); + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + + if ($amount == $transaction_amount) { + $param = ['Token'=>Request::input('token'), 'RefNum'=>Request::input('RefNum')]; + $response = $this->client->post($this->settings->apiVerificationUrl, [ + 'json'=> array_merge( + ['WSContext'=> $this->getWsContext()], + $param + )]); + + $response_data = json_decode($response->getBody()->getContents()); + if ($response_data->Result != 'erSucceed') { + throw new InvalidPaymentException($response_data->Result); + } elseif ($amount != $response_data->Amount) { + $this->client->post( + $this->settings->apiReverseAmountUrl, + [ + 'json'=> [ + 'WSContext'=> $this->getWsContext(), + $param + ] + ] + ); + throw new InvalidPaymentException('مبلغ تراکنش برگشت داده شد'); + } + } + + return $this->createReceipt(Request::input('ResNum')); + } + + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId): Receipt + { + $receipt = new Receipt('fanavacard', $referenceId); + $receipt->detail([ + 'ResNum'=>Request::input('ResNum'), + 'RefNum'=>Request::input('RefNum'), + 'token'=>Request::input('token'), + 'CustomerRefNum'=>Request::input('CustomerRefNum'), + 'CardMaskPan'=>Request::input('CardMaskPan'), + 'transactionAmount'=>Request::input('transactionAmount'), + 'emailAddress'=>Request::input('emailAddress'), + 'mobileNo'=>Request::input('mobileNo'), + ]); + return $receipt; + } + + /** + * call create token request + * + * @return array + * @throws PurchaseFailedException + */ + public function getToken(): array + { + $response = $this->client->request('POST', $this->settings->apiPurchaseUrl, [ + 'json'=>[ + 'WSContext'=> $this->getWsContext(), + 'TransType'=>'EN_GOODS', + 'ReserveNum'=>$this->invoice->getDetail('invoice_number') ?? crc32($this->invoice->getUuid()), + 'Amount'=> $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'RedirectUrl'=>$this->settings->callbackUrl, + ]]); + + if ($response->getStatusCode() != 200) { + throw new PurchaseFailedException( + "cant get token | {$response->getBody()->getContents()}", + $response->getStatusCode() + ); + } + + $response_data = json_decode($response->getBody()->getContents()); + if ($response_data->Result != 'erSucceed') { + throw new PurchaseFailedException( + "cant get token | {$response->getBody()->getContents()}", + $response->getStatusCode() + ); + } + + return (array) $response_data; + } + + private function httpClientInit(): void + { + $this->client = new Client([ + 'curl'=>[CURLOPT_SSL_CIPHER_LIST=>'DEFAULT@SECLEVEL=1',], + 'verify' => false, + 'base_uri' => $this->settings->baseUri, + 'headers' => ['Content-Type' => 'application/json',], + ]); + } + + private function getWsContext(): array + { + return ['UserId' => $this->settings->username, 'Password' => $this->settings->password]; + } +} diff --git a/src/Drivers/Gooyapay/Gooyapay.php b/src/Drivers/Gooyapay/Gooyapay.php new file mode 100644 index 0000000..e672823 --- /dev/null +++ b/src/Drivers/Gooyapay/Gooyapay.php @@ -0,0 +1,198 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function purchase() + { + $details = $this->invoice->getDetails(); + + $name = ''; + if (!empty($details['name'])) { + $name = $details['name']; + } + + $mobile = ''; + if (!empty($details['mobile'])) { + $mobile = $details['mobile']; + } elseif (!empty($details['phone'])) { + $mobile = $details['phone']; + } + + $email = ''; + if (!empty($details['mail'])) { + $email = $details['mail']; + } elseif (!empty($details['email'])) { + $email = $details['email']; + } + + $desc = ''; + if (!empty($details['desc'])) { + $desc = $details['desc']; + } elseif (!empty($details['description'])) { + $desc = $details['description']; + } + + $amount = $this->invoice->getAmount(); + if ($this->settings->currency != 'T') { + $amount /= 10; + } + $amount = intval(ceil($amount)); + + $orderId = crc32($this->invoice->getUuid()); + if (!empty($details['orderId'])) { + $orderId = $details['orderId']; + } elseif (!empty($details['order_id'])) { + $orderId = $details['order_id']; + } + + $data = array( + "MerchantID" => $this->settings->merchantId, + "Amount" => $amount, + "InvoiceID" => $orderId, + "Description" => $desc, + "FullName" => $name, + "Email" => $email, + "Mobile" => $mobile, + "CallbackURL" => $this->settings->callbackUrl, + ); + + $response = $this->client->request('POST', $this->settings->apiPurchaseUrl, ["json" => $data, "http_errors" => false]); + + $body = json_decode($response->getBody()->getContents(), false); + + if ($body->Status != 100) { + // some error has happened + throw new PurchaseFailedException($body->Message); + } + + $this->invoice->transactionId($body->Authority); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay() : RedirectionForm + { + $payUrl = $this->settings->apiPaymentUrl.$this->invoice->getTransactionId(); + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return mixed|void + * + * @throws InvalidPaymentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verify() : ReceiptInterface + { + $PaymentStatus = Request::input('PaymentStatus'); + if ($PaymentStatus != 'OK') { + throw new InvalidPaymentException('تراکنش از سوی کاربر لغو شد', $PaymentStatus); + } + + $Authority = Request::input('Authority'); + $InvoiceID = Request::input('InvoiceID'); + + if ($Authority != $this->invoice->getTransactionId()) { + throw new InvalidPaymentException('اطلاعات تراکنش دریافتی با صورتحساب همخوانی ندارد', 'DATAMISMATCH'); + } + + $amount = $this->invoice->getAmount(); + if ($this->settings->currency != 'T') { + $amount /= 10; + } + $amount = intval(ceil($amount)); + + //start verfication + $data = array( + "MerchantID" => $this->settings->merchantId, + "Authority" => $Authority, + "Amount" => $amount, + ); + + $response = $this->client->request('POST', $this->settings->apiVerificationUrl, ["json" => $data, "http_errors" => false]); + + $body = json_decode($response->getBody()->getContents(), false); + + if ($body->Status != 100) { + throw new InvalidPaymentException($body->Message, $body->Status); + } + + $receipt = new Receipt('gooyapay', $body->RefID); + + $receipt->detail([ + 'Authority' => $data['Authority'], + 'InvoiceID1' => $InvoiceID, + 'InvoiceID2' => $body->InvoiceID, + 'Amount1' => $data['Amount'], + 'Amount2' => $body->Amount, + 'CardNumber' => $body->MaskCardNumber, + 'PaymentTime' => $body->PaymentTime, + 'PaymenterIP' => $body->BuyerIP + ]); + + return $receipt; + } +} diff --git a/src/Drivers/Idpay/Idpay.php b/src/Drivers/Idpay/Idpay.php index 7420c06..ea5e1ba 100644 --- a/src/Drivers/Idpay/Idpay.php +++ b/src/Drivers/Idpay/Idpay.php @@ -84,7 +84,7 @@ public function purchase() $data = array( 'order_id' => $this->invoice->getUuid(), - 'amount' => $this->invoice->getAmount(), + 'amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial 'name' => $details['name'] ?? null, 'phone' => $phone, 'mail' => $mail, @@ -177,7 +177,10 @@ public function verify() : ReceiptInterface $this->notVerified($errorCode); } - return $this->createReceipt($body['track_id']); + $receipt = $this->createReceipt($body['track_id']); + $receipt->detail($body); + + return $receipt; } /** @@ -233,9 +236,9 @@ private function notVerified($status) "54" => "مدت زمان تایید پرداخت سپری شده است.", ); if (array_key_exists($status, $translations)) { - throw new InvalidPaymentException($translations[$status]); + throw new InvalidPaymentException($translations[$status], (int)$status); } else { - throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.'); + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); } } } diff --git a/src/Drivers/Irankish/Irankish.php b/src/Drivers/Irankish/Irankish.php index babb211..0e14dc1 100644 --- a/src/Drivers/Irankish/Irankish.php +++ b/src/Drivers/Irankish/Irankish.php @@ -40,6 +40,25 @@ public function __construct(Invoice $invoice, $settings) $this->settings = (object) $settings; } + private function generateAuthenticationEnvelope($pubKey, $terminalID, $password, $amount) + { + $data = $terminalID . $password . str_pad($amount, 12, '0', STR_PAD_LEFT) . '00'; + $data = hex2bin($data); + $AESSecretKey = openssl_random_pseudo_bytes(16); + $ivlen = openssl_cipher_iv_length($cipher = "AES-128-CBC"); + $iv = openssl_random_pseudo_bytes($ivlen); + $ciphertext_raw = openssl_encrypt($data, $cipher, $AESSecretKey, $options = OPENSSL_RAW_DATA, $iv); + $hmac = hash('sha256', $ciphertext_raw, true); + $crypttext = ''; + + openssl_public_encrypt($AESSecretKey . $hmac, $crypttext, $pubKey); + + return array( + "data" => bin2hex($crypttext), + "iv" => bin2hex($iv), + ); + } + /** * Purchase Invoice. * @@ -56,29 +75,51 @@ public function purchase() $description = $this->settings->description; } - $data = array( - 'amount' => $this->invoice->getAmount() * 10, // convert to rial - 'merchantId' => $this->settings->merchantId, - 'description' => $description, - 'revertURL' => $this->settings->callbackUrl, - 'invoiceNo' => crc32($this->invoice->getUuid()), - 'paymentId' => crc32($this->invoice->getUuid()), - 'specialPaymentId' => crc32($this->invoice->getUuid()), - ); + $pubKey = $this->settings->pubKey; + $terminalID = $this->settings->terminalId; + $password = $this->settings->password; + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial - $soap = new \SoapClient( - $this->settings->apiPurchaseUrl - ); - $response = $soap->MakeToken($data); + $token = $this->generateAuthenticationEnvelope($pubKey, $terminalID, $password, $amount); - if ($response->MakeTokenResult->result != false) { - $this->invoice->transactionId($response->MakeTokenResult->token); - } else { + $data = []; + $data['request'] = [ + 'acceptorId' => $this->settings->acceptorId, + 'amount' => $amount, + 'billInfo' => null, + "paymentId" => null, + "requestId" => uniqid(), + "requestTimestamp" => time(), + "revertUri" => $this->settings->callbackUrl, + "terminalId" => $this->settings->terminalId, + "transactionType" => "Purchase", + ]; + $data['authenticationEnvelope'] = $token; + $dataString = json_encode($data); + + $ch = curl_init($this->settings->apiPurchaseUrl); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $dataString); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($dataString) + )); + curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'DEFAULT@SECLEVEL=1'); + + $result = curl_exec($ch); + curl_close($ch); + + $response = json_decode($result, JSON_OBJECT_AS_ARRAY); + + if (!$response || $response["responseCode"] != "00") { // error has happened - $message = $response->MakeTokenResult->message ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.'; + $message = $response["description"] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.'; throw new PurchaseFailedException($message); } + $this->invoice->transactionId($response['result']['token']); + // return the transaction's id return $this->invoice->getTransactionId(); } @@ -95,8 +136,7 @@ public function pay() : RedirectionForm return $this->redirectWithForm( $payUrl, [ - 'token' => $this->invoice->getTransactionId(), - 'merchantId' => $this->settings->merchantId, + 'tokenIdentity' => $this->invoice->getTransactionId() ], 'POST' ); @@ -108,28 +148,42 @@ public function pay() : RedirectionForm * @return ReceiptInterface * * @throws InvalidPaymentException - * @throws \SoapFault */ public function verify() : ReceiptInterface { - $data = array( - 'merchantId' => $this->settings->merchantId, - 'sha1Key' => $this->settings->sha1Key, - 'token' => $this->invoice->getTransactionId(), - 'amount' => $this->invoice->getAmount() * 10, // convert to rial - 'referenceNumber' => Request::get('referenceId'), - ); + $status = Request::input('responseCode'); + if (Request::input('responseCode') != "00") { + return $this->notVerified($status); + } + + $data = [ + 'terminalId' => $this->settings->terminalId, + 'retrievalReferenceNumber' => Request::input('retrievalReferenceNumber'), + 'systemTraceAuditNumber' => Request::input('systemTraceAuditNumber'), + 'tokenIdentity' => Request::input('token'), + ]; - $soap = new \SoapClient($this->settings->apiVerificationUrl); - $response = $soap->KicccPaymentsVerification($data); + $dataString = json_encode($data); - $status = (int) ($response->KicccPaymentsVerificationResult); + $ch = curl_init($this->settings->apiVerificationUrl); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $dataString); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($dataString) + )); + curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'DEFAULT@SECLEVEL=1'); - if ($status != $data['amount']) { + $result = curl_exec($ch); + if ($result === false || !$data['retrievalReferenceNumber']) { $this->notVerified($status); } + curl_close($ch); + + $response = json_decode($result, JSON_OBJECT_AS_ARRAY); - return $this->createReceipt($data['referenceNumber']); + return $this->createReceipt($data['retrievalReferenceNumber']); } /** @@ -141,9 +195,7 @@ public function verify() : ReceiptInterface */ protected function createReceipt($referenceId) { - $receipt = new Receipt('irankish', $referenceId); - - return $receipt; + return new Receipt('irankish', $referenceId); } /** @@ -154,50 +206,69 @@ protected function createReceipt($referenceId) */ private function notVerified($status) { - $translations = array( - 110 => 'دارنده کارت انصراف داده است', - 120 => 'موجودی حساب کافی نمی باشد', - 121 => 'مبلغ تراکنشهای کارت بیش از حد مجاز است', - 130 => 'اطلاعات کارت نادرست می باشد', - 131 => 'رمز کارت اشتباه است', - 132 => 'کارت مسدود است', - 133 => 'کارت منقضی شده است', - 140 => 'زمان مورد نظر به پایان رسیده است', - 150 => 'خطای داخلی بانک به وجود آمده است', - 160 => 'خطای انقضای کارت به وجود امده یا اطلاعات CVV2 اشتباه است', - 166 => 'بانک صادر کننده کارت شما مجوز انجام تراکنش را صادر نکرده است', - 167 => 'خطا در مبلغ تراکنش', - 200 => 'مبلغ تراکنش بیش از حدنصاب مجاز', - 201 => 'مبلغ تراکنش بیش از حدنصاب مجاز برای روز کاری', - 202 => 'مبلغ تراکنش بیش از حدنصاب مجاز برای ماه کاری', - 203 => 'تعداد تراکنشهای مجاز از حد نصاب گذشته است', - 499 => 'خطای سیستمی ، لطفا مجددا تالش فرمایید', - 500 => 'خطا در تایید تراکنش های خرد شده', - 501 => 'خطا در تایید تراکتش ، ویزگی تایید خودکار', - 502 => 'آدرس آی پی نا معتبر', - 503 => 'پذیرنده در حالت تستی می باشد ، مبلغ نمی تواند بیش از حد مجاز تایین شده برای پذیرنده تستی باشد', - 504 => 'خطا در بررسی الگوریتم شناسه پرداخت', - 505 => 'مدت زمان الزم برای انجام تراکنش تاییدیه به پایان رسیده است', - 506 => 'ذیرنده یافت نشد', - 507 => 'توکن نامعتبر/طول عمر توکن منقضی شده است', - 508 => 'توکن مورد نظر یافت نشد و یا منقضی شده است', - 509 => 'خطا در پارامترهای اجباری خرید تسهیم شده', - 510 => 'خطا در تعداد تسهیم | مبالغ کل تسهیم مغایر با مبلغ کل ارائه شده | خطای شماره ردیف تکراری', - 511 => 'حساب مسدود است', - 512 => 'حساب تعریف نشده است', - 513 => 'شماره تراکنش تکراری است', - -20 => 'در درخواست کارکتر های غیر مجاز وجو دارد', - -30 => 'تراکنش قبلا برگشت خورده است', - -50 => 'طول رشته درخواست غیر مجاز است', - -51 => 'در در خواست خطا وجود دارد', - -80 => 'تراکنش مورد نظر یافت نشد', - -81 => ' خطای داخلی بانک', - -90 => 'تراکنش قبلا تایید شده است' - ); + $translations = [ + 5 => 'از انجام تراکنش صرف نظر شد', + 17 => 'از انجام تراکنش صرف نظر شد', + 3 => 'پذیرنده فروشگاهی نامعتبر است', + 64 => 'مبلغ تراکنش نادرست است، جمع مبالغ تقسیم وجوه برابر مبلغ کل تراکنش نمی باشد', + 94 => 'تراکنش تکراری است', + 25 => 'تراکنش اصلی یافت نشد', + 77 => 'روز مالی تراکنش نا معتبر است', + 63 => 'کد اعتبار سنجی پیام نا معتبر است', + 97 => 'کد تولید کد اعتبار سنجی نا معتبر است', + 30 => 'فرمت پیام نادرست است', + 86 => 'شتاب در حال Off Sign است', + 55 => 'رمز کارت نادرست است', + 40 => 'عمل درخواستی پشتیبانی نمی شود', + 57 => 'انجام تراکنش مورد درخواست توسط پایانه انجام دهنده مجاز نمی باشد', + 58 => 'انجام تراکنش مورد درخواست توسط پایانه انجام دهنده مجاز نمی باشد', +// 63 => 'تمهیدات امنیتی نقض گردیده است', + 96 => 'قوانین سامانه نقض گردیده است ، خطای داخلی سامانه', + 2 => 'تراکنش قبال برگشت شده است', + 54 => 'تاریخ انقضا کارت سررسید شده است', + 62 => 'کارت محدود شده است', + 75 => 'تعداد دفعات ورود رمز اشتباه از حد مجاز فراتر رفته است', + 14 => 'اطالعات کارت صحیح نمی باشد', + 51 => 'موجودی حساب کافی نمی باشد', + 56 => 'اطالعات کارت یافت نشد', + 61 => 'مبلغ تراکنش بیش از حد مجاز است', + 65 => 'تعداد دفعات انجام تراکنش بیش از حد مجاز است', + 78 => 'کارت فعال نیست', + 79 => 'حساب متصل به کارت بسته یا دارای اشکال است', + 42 => 'کارت یا حساب مبدا در وضعیت پذیرش نمی باشد', +// 42 => 'کارت یا حساب مقصد در وضعیت پذیرش نمی باشد', + 31 => 'عدم تطابق کد ملی خریدار با دارنده کارت', + 98 => 'سقف استفاده از رمز دوم ایستا به پایان رسیده است', + 901 => 'درخواست نا معتبر است )Tokenization(', + 902 => 'پارامترهای اضافی درخواست نامعتبر می باشد )Tokenization(', + 903 => 'شناسه پرداخت نامعتبر می باشد )Tokenization(', + 904 => 'اطالعات مرتبط با قبض نا معتبر می باشد )Tokenization(', + 905 => 'شناسه درخواست نامعتبر می باشد )Tokenization(', + 906 => 'درخواست تاریخ گذشته است )Tokenization(', + 907 => 'آدرس بازگشت نتیجه پرداخت نامعتبر می باشد )Tokenization(', + 909 => 'پذیرنده نامعتبر می باشد)Tokenization(', + 910 => 'پارامترهای مورد انتظار پرداخت تسهیمی تامین نگردیده است)Tokenization(', + 911 => 'پارامترهای مورد انتظار پرداخت تسهیمی نا معتبر یا دارای اشکال می باشد)Tokenization(', + 912 => 'تراکنش درخواستی برای پذیرنده فعال نیست )Tokenization(', + 913 => 'تراکنش تسهیم برای پذیرنده فعال نیست )Tokenization(', + 914 => 'آدرس آی پی دریافتی درخواست نا معتبر می باشد', + 915 => 'شماره پایانه نامعتبر می باشد )Tokenization(', + 916 => 'شماره پذیرنده نا معتبر می باشد )Tokenization(', + 917 => 'نوع تراکنش اعالم شده در خواست نا معتبر می باشد )Tokenization(', + 918 => 'پذیرنده فعال نیست)Tokenization(', + 919 => 'مبالغ تسهیمی ارائه شده با توجه به قوانین حاکم بر وضعیت تسهیم پذیرنده ، نا معتبر است )Tokenization(', + 920 => 'شناسه نشانه نامعتبر می باشد', + 921 => 'شناسه نشانه نامعتبر و یا منقضی شده است', + 922 => 'نقض امنیت درخواست )Tokenization(', + 923 => 'ارسال شناسه پرداخت در تراکنش قبض مجاز نیست)Tokenization(', + 928 => 'مبلغ مبادله شده نا معتبر می باشد)Tokenization(', + 929 => 'شناسه پرداخت ارائه شده با توجه به الگوریتم متناظر نا معتبر می باشد)Tokenization(', + 930 => 'کد ملی ارائه شده نا معتبر می باشد)Tokenization(' + ]; if (array_key_exists($status, $translations)) { - throw new InvalidPaymentException($translations[$status]); + throw new InvalidPaymentException($translations[$status], (int)$status); } else { - throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.'); + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); } } } diff --git a/src/Drivers/Jibit/Jibit.php b/src/Drivers/Jibit/Jibit.php new file mode 100644 index 0000000..782e165 --- /dev/null +++ b/src/Drivers/Jibit/Jibit.php @@ -0,0 +1,127 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->jibit = new JibitClient( + $this->settings->apiKey, + $this->settings->apiSecret, + $this->settings->apiPaymentUrl, + $this->settings->tokenStoragePath + ); + } + + /** + * Purchase invoice + * + * @return string + * @throws PurchaseFailedException + */ + public function purchase() + { + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // Convert to Rial + + $requestResult = $this->jibit->paymentRequest( + $amount, + $this->invoice->getUuid(), + $this->invoice->getDetail('mobile'), + $this->settings->callbackUrl + ); + + + if (! empty($requestResult['pspSwitchingUrl'])) { + $this->paymentUrl = $requestResult['pspSwitchingUrl']; + } + + if (! empty($requestResult['errors'])) { + $errMsgs = array_map(function ($err) { + return $err['code']; + }, $requestResult['errors']); + + throw new PurchaseFailedException(implode('\n', $errMsgs)); + } + + $purchaseId = $requestResult['purchaseId']; + $referenceNumber = $requestResult['clientReferenceNumber']; + + $this->invoice->detail('referenceNumber', $referenceNumber); + $this->invoice->transactionId($purchaseId); + + return $purchaseId; + } + + /** + * Pay invoice + * + * @return RedirectionForm + */ + public function pay() : RedirectionForm + { + $url = $this->paymentUrl; + + return $this->redirectWithForm($url, [], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * @throws InvalidPaymentException + * @throws PurchaseFailedException + */ + public function verify(): ReceiptInterface + { + $purchaseId = $this->invoice->getTransactionId(); + + $requestResult = $this->jibit->paymentVerify($purchaseId); + + if (! empty($requestResult['status']) && $requestResult['status'] === 'SUCCESSFUL') { + $order = $this->jibit->getOrderById($purchaseId); + + $receipt = new Receipt('jibit', $purchaseId); + return $receipt->detail('payerCard', $order['elements']['payerCardNumber'] ?? ''); + } + + throw new InvalidPaymentException('Payment encountered an issue.'); + } +} diff --git a/src/Drivers/Jibit/JibitClient.php b/src/Drivers/Jibit/JibitClient.php new file mode 100644 index 0000000..3dce45e --- /dev/null +++ b/src/Drivers/Jibit/JibitClient.php @@ -0,0 +1,301 @@ +baseUrl = $baseUrl; + $this->apiKey = $apiKey; + $this->secretKey = $secretKey; + $this->cache = new FileCache( + new CacheOptions([ + 'filestorage' => $cachePath, + ]) + ); + } + + /** + * Request payment + * + * @param int $amount + * @param string $referenceNumber + * @param string $userIdentifier + * @param string $callbackUrl + * @param string $currency + * @param null $description + * @param $additionalData + * @return bool|mixed|string + * @throws PurchaseFailedException + */ + public function paymentRequest($amount, $referenceNumber, $userIdentifier, $callbackUrl, $currency = 'IRR', $description = null, $additionalData = null) + { + $this->generateToken(); + + $data = [ + 'additionalData' => $additionalData, + 'amount' => $amount, + 'callbackUrl' => $callbackUrl, + 'clientReferenceNumber' => $referenceNumber, + 'currency' => $currency, + 'userIdentifier' => $userIdentifier, + 'description' => $description, + ]; + + return $this->callCurl('/purchases', $data, true); + } + + /** + * + * Get order by ID + * @param $id + * @return bool|mixed|string + * @throws PurchaseFailedException + */ + public function getOrderById($id) + { + return $this->callCurl('/purchases?purchaseId=' . $id, [], true, 0, 'GET'); + } + + /** + * Generate token + * + * @param bool $isForce + * @return string + * @throws PurchaseFailedException + * @throws InvalidArgumentException + */ + private function generateToken($isForce = false) + { + if ($isForce === false && $this->cache->has('accessToken')) { + return $this->setAccessToken($this->cache->get('accessToken')); + } elseif ($this->cache->has('refreshToken')) { + $refreshToken = $this->refreshTokens(); + + if ($refreshToken !== 'ok') { + return $this->generateNewToken(); + } + } else { + return $this->generateNewToken(); + } + + throw new PurchaseFailedException('Token generation encountered an error.'); + } + + /** + * Refresh tokens + * @throws PurchaseFailedException + * @throws InvalidArgumentException + */ + private function refreshTokens() + { + $data = [ + 'accessToken' => str_replace('Bearer ', '', $this->cache->get('accessToken')), + 'refreshToken' => $this->cache->get('refreshToken'), + ]; + + $result = $this->callCurl('/tokens/refresh', $data, false); + + if (empty($result['accessToken'])) { + throw new PurchaseFailedException('Refresh token encountered an error.'); + } + + if (!empty($result['accessToken'])) { + $this->cache->set('accessToken', 'Bearer ' . $result['accessToken'], 24 * 60 * 60 - 60); + $this->cache->set('refreshToken', $result['refreshToken'], 48 * 60 * 60 - 60); + + $this->setAccessToken('Bearer ' . $result['accessToken']); + $this->setRefreshToken($result['refreshToken']); + + return 'ok'; + } + + throw new PurchaseFailedException('Refresh token encountered an error.'); + } + + /** + * Call curl + * + * @param $url + * @param $arrayData + * @param bool $haveAuth + * @param int $try + * @param string $method + * @return bool|mixed|string + * @throws PurchaseFailedException + */ + private function callCurl($url, $arrayData, $haveAuth = false, $try = 0, $method = 'POST') + { + $data = $arrayData; + $jsonData = json_encode($data); + $accessToken = ''; + + if ($haveAuth) { + $accessToken = $this->getAccessToken(); + } + + $ch = curl_init($this->baseUrl . $url); + curl_setopt($ch, CURLOPT_USERAGENT, 'Jibit.class Rest Api'); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: ' . $accessToken, + 'Content-Length: ' . strlen($jsonData) + ]); + + $result = curl_exec($ch); + $err = curl_error($ch); + $result = json_decode($result, true); + curl_close($ch); + + if ($err) { + throw new PurchaseFailedException('cURL Error #:' . $err); + } + + if (empty($result['errors'])) { + return $result; + } + + if ($haveAuth === true && $result['errors'][0]['code'] === 'security.auth_required') { + $this->generateToken(true); + + if ($try === 0) { + return $this->callCurl($url, $arrayData, $haveAuth, 1, $method); + } + + throw new PurchaseFailedException('Authentication encountered an error.'); + } + + return $result; + } + + /** + * Get access token + * + * @return mixed + */ + public function getAccessToken() + { + return $this->accessToken; + } + + /** + * Set access token + * + * @param mixed $accessToken + */ + public function setAccessToken($accessToken) + { + $this->accessToken = $accessToken; + } + + /** + * Set refresh token + * + * @param mixed $refreshToken + */ + public function setRefreshToken($refreshToken) + { + $this->refreshToken = $refreshToken; + } + + /** + * Generate new token + * + * @return string + * @throws PurchaseFailedException + * @throws InvalidArgumentException + */ + private function generateNewToken() + { + $data = [ + 'apiKey' => $this->apiKey, + 'secretKey' => $this->secretKey, + ]; + + $result = $this->callCurl('/tokens', $data); + + if (empty($result['accessToken'])) { + throw new PurchaseFailedException('Token generation encoutered an error.'); + } + + if (! empty($result['accessToken'])) { + $this->cache->set('accessToken', 'Bearer ' . $result['accessToken'], 24 * 60 * 60 - 60); + $this->cache->set('refreshToken', $result['refreshToken'], 48 * 60 * 60 - 60); + + $this->setAccessToken('Bearer ' . $result['accessToken']); + $this->setRefreshToken($result['refreshToken']); + + return 'ok'; + } + + throw new PurchaseFailedException('Token generation encoutered an error.'); + } + + /** + * Verify payment + * + * @param string $purchaseId + * @return bool|mixed|string + * @throws PurchaseFailedException + */ + public function paymentVerify($purchaseId) + { + $this->generateToken(); + $data = []; + + return $this->callCurl('/purchases/' . $purchaseId . '/verify', $data, true, 0, 'GET'); + } +} diff --git a/src/Drivers/Local/Local.php b/src/Drivers/Local/Local.php new file mode 100644 index 0000000..b6c1a04 --- /dev/null +++ b/src/Drivers/Local/Local.php @@ -0,0 +1,184 @@ +invoice($invoice); + $this->settings = (object) $settings; + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + */ + public function purchase(): string + { + // throw PurchaseFailedException if set in invoice message + if ($message = $this->invoice->getDetail('failedPurchase')) { + throw new PurchaseFailedException($message); + } + + // set a randomly grenerated transactionId + $transactionId = mt_rand(1000000, 9999999); + $this->invoice->transactionId($transactionId); + + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + RedirectionForm::setViewPath(dirname(__DIR__).'/../../resources/views/local-form.php'); + + return new RedirectionForm('', $this->getFormData(), 'POST'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + */ + public function verify(): ReceiptInterface + { + $data = array( + 'transactionId' => Request::input('transactionId'), + 'cancel' => Request::input('cancel') + ); + + $success = $data['transactionId'] && !$data['cancel']; + + if (!$success) { + $this->notVerified(0); + } + + $receipt = $this->createReceipt($data['transactionId']); + $receipt->detail([ + 'orderId' => $this->invoice->getDetail('orderId') ?? mt_rand(1111, 9999), + 'traceNo' => $this->invoice->getDetail('traceNo') ?? mt_rand(11111, 99999), + 'referenceNo' => $data['transactionId'], + 'cardNo' => $this->invoice->getDetail('cartNo') ?? mt_rand(1111, 9999), + ]); + + return $receipt; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId): Receipt + { + return new Receipt('local', $referenceId); + } + + /** + * Populate payment form data + * + * + * @return array + */ + protected function getFormData(): array + { + return [ + 'orderId' => $this->invoice->getDetail('orderId'), + 'price' => number_format($this->invoice->getAmount()), + 'successUrl' => $this->addUrlQuery($this->settings->callbackUrl, [ + 'transactionId' => $this->invoice->getTransactionId(), + ]), + 'cancelUrl' => $this->addUrlQuery($this->settings->callbackUrl, [ + 'transactionId' => $this->invoice->getTransactionId(), + 'cancel' => 'true', + ]), + 'title' => $this->settings->title, + 'description' => $this->settings->description, + 'orderLabel' => $this->settings->orderLabel, + 'amountLabel' => $this->settings->amountLabel, + 'payButton' => $this->settings->payButton, + 'cancelButton' => $this->settings->cancelButton, + ]; + } + + + /** + * Add array parameters as url query + * + * @param $url + * @param $params + * + * @return string + */ + protected function addUrlQuery($url, $params): string + { + $urlWithQuery = $url; + foreach ($params as $key => $value) { + $urlWithQuery .= (parse_url($urlWithQuery, PHP_URL_QUERY) ? '&' : '?') . "{$key}={$value}"; + } + return $urlWithQuery; + } + + + /** + * Trigger an exception + * + * @param $status + * + * @throws InvalidPaymentException + */ + private function notVerified($status) + { + $translations = array( + 0 => 'تراکنش توسط خریدار لغو شده است.', + ); + + if (array_key_exists($status, $translations)) { + throw new InvalidPaymentException($translations[$status], (int)$status); + } else { + throw new InvalidPaymentException('تراکنش با خطا مواجه شد.', (int)$status); + } + } +} diff --git a/src/Drivers/Minipay/Minipay.php b/src/Drivers/Minipay/Minipay.php new file mode 100644 index 0000000..be88376 --- /dev/null +++ b/src/Drivers/Minipay/Minipay.php @@ -0,0 +1,188 @@ +invoice($invoice); + $this->client = new Client(); + $this->settings = (object)$settings; + } + + /** + * Retrieve data from details using its name. + * + * @return string + */ + private function extractDetails($name) + { + return empty($this->invoice->getDetails()[$name]) ? null : $this->invoice->getDetails()[$name]; + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + $metadata = []; + + if (!empty($this->invoice->getDetails()['description'])) { + $description = $this->invoice->getDetails()['description']; + } else { + $description = $this->settings->description; + } + + $data = [ + "merchant_id" => $this->settings->merchantId, + "amount" => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + "callback_url" => $this->settings->callbackUrl, + "description" => $description, + "metadata" => array_merge($this->invoice->getDetails() ?? [], $metadata), + ]; + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPurchaseUrl, + [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ] + ); + + $result = json_decode($response->getBody()->getContents(), true); + if ($response->getStatusCode() != 200) { + $message = $result['messages'][0]['text'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.'; + throw new PurchaseFailedException($message, $response->getStatusCode()); + } + + $this->invoice->transactionId($result['data']["authority"]); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + $payUrl = $this->settings->apiPaymentUrl . '?authority=' . $this->invoice->getTransactionId(); + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return mixed|void + * + * @throws InvalidPaymentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verify(): ReceiptInterface + { + $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); + $data = [ + "merchant_id" => $this->settings->merchantId, + "authority" => $authority, + "amount" => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + ]; + + $response = $this->client->request( + 'POST', + $this->settings->apiVerificationUrl, + [ + 'json' => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ] + ); + + $result = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200) { + $message = $result['messages'][0]['text'] ?? 'تراکنش تایید نشد.'; + throw new InvalidPaymentException($message, $response->getStatusCode()); + } + + $refId = $result['data']['id']; + + $receipt = $this->createReceipt($refId); + $receipt->detail([ + 'ref_id' => $refId, + 'installments_count' => $result['data']['installments_count'] ?? null, + 'price' => $result['data']['price'] ?? null, + 'credit_price' => $result['data']['credit_price'] ?? null, + ]); + + return $receipt; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return ReceiptInterface + */ + protected function createReceipt($referenceId): ReceiptInterface + { + $receipt = new Receipt('minipay', $referenceId); + + return $receipt; + } +} diff --git a/src/Drivers/Nextpay/Nextpay.php b/src/Drivers/Nextpay/Nextpay.php index fd5d1ae..ea83e9b 100644 --- a/src/Drivers/Nextpay/Nextpay.php +++ b/src/Drivers/Nextpay/Nextpay.php @@ -4,9 +4,9 @@ use GuzzleHttp\Client; use Shetabit\Multipay\Abstracts\Driver; +use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Exceptions\InvalidPaymentException; use Shetabit\Multipay\Exceptions\PurchaseFailedException; -use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Invoice; use Shetabit\Multipay\Receipt; use Shetabit\Multipay\RedirectionForm; @@ -45,7 +45,7 @@ class Nextpay extends Driver public function __construct(Invoice $invoice, $settings) { $this->invoice($invoice); - $this->settings = (object) $settings; + $this->settings = (object)$settings; $this->client = new Client(); } @@ -59,12 +59,36 @@ public function __construct(Invoice $invoice, $settings) */ public function purchase() { - $data = array( + $data = [ 'api_key' => $this->settings->merchantId, - 'order_id' => intval(1, time()).crc32($this->invoice->getUuid()), - 'amount' => $this->invoice->getAmount(), + 'order_id' => intval(1, time()) . crc32($this->invoice->getUuid()), + 'amount' => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman 'callback_uri' => $this->settings->callbackUrl, - ); + ]; + + if (isset($this->invoice->getDetails()['customer_phone'])) { + $data['customer_phone'] = $this->invoice->getDetails()['customer_phone']; + } + + if (isset($this->invoice->getDetails()['custom_json_fields'])) { + $data['custom_json_fields'] = $this->invoice->getDetails()['custom_json_fields']; + } + + if (isset($this->invoice->getDetails()['payer_name'])) { + $data['payer_name'] = $this->invoice->getDetails()['payer_name']; + } + + if (isset($this->invoice->getDetails()['payer_desc'])) { + $data['payer_desc'] = $this->invoice->getDetails()['payer_desc']; + } + + if (isset($this->invoice->getDetails()['auto_verify'])) { + $data['auto_verify'] = $this->invoice->getDetails()['auto_verify']; + } + + if (isset($this->invoice->getDetails()['allowed_card'])) { + $data['allowed_card'] = $this->invoice->getDetails()['allowed_card']; + } $response = $this ->client @@ -78,10 +102,10 @@ public function purchase() ); $body = json_decode($response->getBody()->getContents(), true); + $code = isset($body['code']) ? $body['code'] : ''; - if (empty($body['code']) || $body['code'] != -1) { - // error has happened - throw new PurchaseFailedException($body['message']); + if ($code != -1) { + throw new PurchaseFailedException($this->translateStatusCode($code), $code); } $this->invoice->transactionId($body['trans_id']); @@ -95,9 +119,9 @@ public function purchase() * * @return RedirectionForm */ - public function pay() : RedirectionForm + public function pay(): RedirectionForm { - $payUrl = $this->settings->apiPaymentUrl.$this->invoice->getTransactionId(); + $payUrl = $this->settings->apiPaymentUrl . $this->invoice->getTransactionId(); return $this->redirectWithForm($payUrl, [], 'GET'); } @@ -110,14 +134,14 @@ public function pay() : RedirectionForm * @throws InvalidPaymentException * @throws \GuzzleHttp\Exception\GuzzleException */ - public function verify() : ReceiptInterface + public function verify(): ReceiptInterface { $transactionId = $this->invoice->getTransactionId() ?? Request::input('trans_id'); $data = [ 'api_key' => $this->settings->merchantId, 'order_id' => Request::input('order_id'), - 'amount' => $this->invoice->getAmount(), + 'amount' => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman 'trans_id' => $transactionId, ]; @@ -133,14 +157,13 @@ public function verify() : ReceiptInterface ); $body = json_decode($response->getBody()->getContents(), true); + $code = isset($body['code']) ? $body['code'] : ''; - if (!isset($body['code']) || $body['code'] != 0) { - $message = $body['message'] ?? 'خطای ناشناخته ای رخت داده است'; - - throw new InvalidPaymentException($message); + if ($code != 0) { + throw new InvalidPaymentException($this->translateStatusCode($code), $code); } - return $this->createReceipt($transactionId); + return $this->createReceipt($body['Shaparak_Ref_Id']); } /** @@ -156,4 +179,78 @@ protected function createReceipt($referenceId) return $receipt; } + + + /** + * Convert status to a readable message. + * + * @param $code + * + * @return string + */ + private function translateStatusCode($code): string + { + $translations = [ + 0 => 'پرداخت تکمیل و با موفقیت انجام شده است', + -1 => 'منتظر ارسال تراکنش و ادامه پرداخت', + -2 => 'پرداخت رد شده توسط کاربر یا بانک', + -3 => 'پرداخت در حال انتظار جواب بانک', + -4 => 'پرداخت لغو شده است', + -20 => 'کد api_key ارسال نشده است', + -21 => 'کد trans_id ارسال نشده است', + -22 => 'مبلغ ارسال نشده', + -23 => 'لینک ارسال نشده', + -24 => 'مبلغ صحیح نیست', + -25 => 'تراکنش قبلا انجام و قابل ارسال نیست', + -26 => 'مقدار توکن ارسال نشده است', + -27 => 'شماره سفارش صحیح نیست', + -28 => 'مقدار فیلد سفارشی [custom_json_fields] از نوع json نیست', + -29 => 'کد بازگشت مبلغ صحیح نیست', + -30 => 'مبلغ کمتر از حداقل پرداختی است', + -31 => 'صندوق کاربری موجود نیست', + -32 => 'مسیر بازگشت صحیح نیست', + -33 => 'کلید مجوز دهی صحیح نیست', + -34 => 'کد تراکنش صحیح نیست', + -35 => 'ساختار کلید مجوز دهی صحیح نیست', + -36 => 'شماره سفارش ارسال نشد است', + -37 => 'شماره تراکنش یافت نشد', + -38 => 'توکن ارسالی موجود نیست', + -39 => 'کلید مجوز دهی موجود نیست', + -40 => 'کلید مجوزدهی مسدود شده است', + -41 => 'خطا در دریافت پارامتر، شماره شناسایی صحت اعتبار که از بانک ارسال شده موجود نیست', + -42 => 'سیستم پرداخت دچار مشکل شده است', + -43 => 'درگاه پرداختی برای انجام درخواست یافت نشد', + -44 => 'پاسخ دریاف شده از بانک نامعتبر است', + -45 => 'سیستم پرداخت غیر فعال است', + -46 => 'درخواست نامعتبر', + -47 => 'کلید مجوز دهی یافت نشد [حذف شده]', + -48 => 'نرخ کمیسیون تعیین نشده است', + -49 => 'تراکنش مورد نظر تکراریست', + -50 => 'حساب کاربری برای صندوق مالی یافت نشد', + -51 => 'شناسه کاربری یافت نشد', + -52 => 'حساب کاربری تایید نشده است', + -60 => 'ایمیل صحیح نیست', + -61 => 'کد ملی صحیح نیست', + -62 => 'کد پستی صحیح نیست', + -63 => 'آدرس پستی صحیح نیست و یا بیش از ۱۵۰ کارکتر است', + -64 => 'توضیحات صحیح نیست و یا بیش از ۱۵۰ کارکتر است', + -65 => 'نام و نام خانوادگی صحیح نیست و یا بیش از ۳۵ کاکتر است', + -66 => 'تلفن صحیح نیست', + -67 => 'نام کاربری صحیح نیست یا بیش از ۳۰ کارکتر است', + -68 => 'نام محصول صحیح نیست و یا بیش از ۳۰ کارکتر است', + -69 => 'آدرس ارسالی برای بازگشت موفق صحیح نیست و یا بیش از ۱۰۰ کارکتر است', + -70 => 'آدرس ارسالی برای بازگشت ناموفق صحیح نیست و یا بیش از ۱۰۰ کارکتر است', + -71 => 'موبایل صحیح نیست', + -72 => 'بانک پاسخگو نبوده است لطفا با نکست پی تماس بگیرید', + -73 => 'مسیر بازگشت دارای خطا میباشد یا بسیار طولانیست', + -90 => 'بازگشت مبلغ بدرستی انجام شد', + -91 => 'عملیات ناموفق در بازگشت مبلغ', + -92 => 'در عملیات بازگشت مبلغ خطا رخ داده است', + -93 => 'موجودی صندوق کاربری برای بازگشت مبلغ کافی نیست', + -94 => 'کلید بازگشت مبلغ یافت نشد', + ]; + $unknownError = 'خطای ناشناخته رخ داده است. در صورت کسر مبلغ از حساب حداکثر پس از 72 ساعت به حسابتان برمیگردد'; + + return array_key_exists($code, $translations) ? $translations[$code] : $unknownError; + } } diff --git a/src/Drivers/Omidpay/Omidpay.php b/src/Drivers/Omidpay/Omidpay.php new file mode 100644 index 0000000..fff1722 --- /dev/null +++ b/src/Drivers/Omidpay/Omidpay.php @@ -0,0 +1,247 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function purchase(): string + { + $data = array( + 'WSContext' => [ + 'UserId' => $this->settings->username, + 'Password' => $this->settings->password, + ], + 'TransType' => 'EN_GOODS', + 'ReserveNum' => $this->invoice->getUuid(), + 'MerchantId' => $this->settings->merchantId, + 'Amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'RedirectUrl' => $this->settings->callbackUrl, + ); + + $response = $this->client->request( + "POST", + $this->settings->apiGenerateTokenUrl, + [ + 'json' => $data, + "headers" => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'User-Agent' => '', + ], + ] + ); + + $responseStatus = $response->getStatusCode(); + + if ($responseStatus != 200) { + throw new PurchaseFailedException($this->translateStatus("unknown_error")); + } + + $jsonBody = $response->getBody()->getContents(); + $responseData = json_decode($jsonBody, true); + + $result = $responseData['Result']; + if (!$this->isSucceed($result)) { + throw new PurchaseFailedException($this->translateStatus($result)); + } + + // set transaction id + $this->invoice->transactionId($responseData['Token']); + + // return the transaction’s id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + $token = $this->invoice->getTransactionId(); + $payUrl = $this->settings->apiPaymentUrl; + + return $this->redirectWithForm($payUrl, ['token' => $token, 'language' => 'fa']); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verify(): ReceiptInterface + { + $token = $this->invoice->getTransactionId() ?? Request::input('token'); + $refNum = Request::input('RefNum'); + + $response = $this->client->request( + "POST", + $this->settings->apiVerificationUrl, + [ + "json" => [ + 'WSContext' => [ + 'UserId' => $this->settings->username, + 'Password' => $this->settings->password, + ], + 'Token' => $token, + 'RefNum' => $refNum + ], + "headers" => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'User-Agent' => '', + ], + ] + ); + + $body = json_decode($response->getBody()->getContents()); + + $result = $body->Result; + if (!$this->isSucceed($result)) { + throw new InvalidPaymentException($this->translateStatus($result)); + } + + return $this->createReceipt($body->RefNum); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId): Receipt + { + return new Receipt('omidpay', $referenceId); + } + + /** + * @param string $status + * @return bool + */ + private function isSucceed(string $status): bool + { + return $status == "erSucceed"; + } + + /** + * Convert status to a readable message. + * + * @param $status + * + * @return mixed|string + */ + private function translateStatus($status): string + { + $translations = [ + 'erSucceed' => 'سرویس با موفقیت اجراء شد.', + 'erAAS_UseridOrPassIsRequired' => 'کد کاربری و رمز الزامی هست.', + 'erAAS_InvalidUseridOrPass' => 'کد کاربری یا رمز صحیح نمی باشد.', + 'erAAS_InvalidUserType' => 'نوع کاربر صحیح نمی‌باشد.', + 'erAAS_UserExpired' => 'کاربر منقضی شده است.', + 'erAAS_UserNotActive' => 'کاربر غیر فعال هست.', + 'erAAS_UserTemporaryInActive' => 'کاربر موقتا غیر فعال شده است.', + 'erAAS_UserSessionGenerateError' => 'خطا در تولید شناسه لاگین', + 'erAAS_UserPassMinLengthError' => 'حداقل طول رمز رعایت نشده است.', + 'erAAS_UserPassMaxLengthError' => 'حداکثر طول رمز رعایت نشده است.', + 'erAAS_InvalidUserCertificate' => 'برای کاربر فایل سرتیفکیت تعریف نشده است.', + 'erAAS_InvalidPasswordChars' => 'کاراکترهای غیر مجاز در رمز', + 'erAAS_InvalidSession' => 'شناسه لاگین معتبر نمی‌باشد ', + 'erAAS_InvalidChannelId' => 'کانال معتبر نمی‌باشد.', + 'erAAS_InvalidParam' => 'پارامترها معتبر نمی‌باشد.', + 'erAAS_NotAllowedToService' => 'کاربر مجوز سرویس را ندارد.', + 'erAAS_SessionIsExpired' => 'شناسه الگین معتبر نمی‌باشد.', + 'erAAS_InvalidData' => 'داده‌ها معتبر نمی‌باشد.', + 'erAAS_InvalidSignature' => 'امضاء دیتا درست نمی‌باشد.', + 'erAAS_InvalidToken' => 'توکن معتبر نمی‌باشد.', + 'erAAS_InvalidSourceIp' => 'آدرس آی پی معتبر نمی‌باشد.', + + 'erMts_ParamIsNull' => 'پارمترهای ورودی خالی می‌باشد.', + 'erMts_UnknownError' => 'خطای ناشناخته', + 'erMts_InvalidAmount' => 'مبلغ معتبر نمی‌باشد.', + 'erMts_InvalidBillId' => 'شناسه قبض معتبر نمی‌باشد.', + 'erMts_InvalidPayId' => 'شناسه پرداخت معتبر نمی‌باشد.', + 'erMts_InvalidEmailAddLen' => 'طول ایمیل معتبر نمی‌باشد.', + 'erMts_InvalidGoodsReferenceIdLen' => 'طول شناسه خرید معتبر نمی‌باشد.', + 'erMts_InvalidMerchantGoodsReferenceIdLen' => 'طول شناسه خرید پذیرنده معتبر نمی‌باشد.', + 'erMts_InvalidMobileNo' => 'فرمت شماره موبایل معتبر نمی‌باشد.', + 'erMts_InvalidPorductId' => 'طول یا فرمت کد محصول معتبر نمی‌باشد.', + 'erMts_InvalidRedirectUrl' => 'طول یا فرمت آدرس صفحه رجوع معتبر نمی‌باشد.', + 'erMts_InvalidReferenceNum' => 'طول یا فرمت شماره رفرنس معتبر نمی‌باشد.', + 'erMts_InvalidRequestParam' => 'پارامترهای درخواست معتبر نمی‌باشد.', + 'erMts_InvalidReserveNum' => 'طول یا فرمت شماره رزرو معتبر نمی‌باشد.', + 'erMts_InvalidSessionId' => 'شناسه الگین معتبر نمی‌باشد.', + 'erMts_InvalidSignature' => 'طول یا فرمت امضاء دیتا معتبر نمی‌باشد.', + 'erMts_InvalidTerminal' => 'کد ترمینال معتبر نمی‌باشد.', + 'erMts_InvalidToken' => 'توکن معتبر نمی‌باشد.', + 'erMts_InvalidTransType' => 'نوع تراکنش معتبر نمی‌باشد.', + 'erMts_InvalidUniqueId' => 'کد یکتا معتبر نمی‌باشد.', + 'erMts_InvalidUseridOrPass' => 'رمز یا کد کاربری معتبر نمی باشد.', + 'erMts_RepeatedBillId' => 'پرداخت قبض تکراری می باشد.', + 'erMts_AASError' => 'کد کاربری و رمز الزامی هست.', + 'erMts_SCMError' => 'خطای سرور مدیریت کانال', + ]; + + $unknownError = 'خطای ناشناخته رخ داده است. در صورت کسر مبلغ از حساب حداکثر پس از 72 ساعت به حسابتان برمیگردد.'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } +} diff --git a/src/Drivers/Parsian/Parsian.php b/src/Drivers/Parsian/Parsian.php index e4df006..0930484 100644 --- a/src/Drivers/Parsian/Parsian.php +++ b/src/Drivers/Parsian/Parsian.php @@ -3,9 +3,9 @@ namespace Shetabit\Multipay\Drivers\Parsian; use Shetabit\Multipay\Abstracts\Driver; +use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Exceptions\InvalidPaymentException; use Shetabit\Multipay\Exceptions\PurchaseFailedException; -use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Invoice; use Shetabit\Multipay\Receipt; use Shetabit\Multipay\RedirectionForm; @@ -80,12 +80,16 @@ public function purchase() */ public function pay() : RedirectionForm { - $payUrl = $this->settings->apiPaymentUrl; + $payUrl = sprintf( + '%s?Token=%s', + $this->settings->apiPaymentUrl, + $this->invoice->getTransactionId() + ); return $this->redirectWithForm( $payUrl, ['Token' => $this->invoice->getTransactionId()], - 'POST' + 'GET' ); } @@ -100,10 +104,10 @@ public function pay() : RedirectionForm public function verify() : ReceiptInterface { $status = Request::input('status'); - $token = Request::input('Token'); + $token = $this->invoice->getTransactionId() ?? Request::input('Token'); - if ($status != 0 || empty($token)) { - throw new InvalidPaymentException('تراکنش توسط کاربر کنسل شده است.'); + if (empty($token)) { + throw new InvalidPaymentException('تراکنش توسط کاربر کنسل شده است.', (int)$status); } $data = $this->prepareVerificationData(); @@ -119,7 +123,7 @@ public function verify() : ReceiptInterface $hasWrongRRN = (!isset($result->RRN) || $result->RRN <= 0); if ($hasWrongStatus || $hasWrongRRN) { $message = 'خطا از سمت بانک با کد '.$result->Status.' رخ داده است.'; - throw new InvalidPaymentException($message); + throw new InvalidPaymentException($message, (int)$result->Status); } return $this->createReceipt($result->RRN); @@ -148,10 +152,10 @@ protected function prepareVerificationData() { $transactionId = $this->invoice->getTransactionId() ?? Request::input('Token'); - return array( - 'LoginAccount' => $this->settings->merchantId, - 'Token' => $transactionId, - ); + return [ + 'LoginAccount' => $this->settings->merchantId, + 'Token' => $transactionId, + ]; } /** @@ -161,18 +165,23 @@ protected function prepareVerificationData() */ protected function preparePurchaseData() { - if (!empty($this->invoice->getDetails()['description'])) { - $description = $this->invoice->getDetails()['description']; - } else { + // The bank suggests that an English description is better + if (empty($description = $this->invoice->getDetail('description'))) { $description = $this->settings->description; } - return array( - 'LoginAccount' => $this->settings->merchantId, - 'Amount' => $this->invoice->getAmount() * 10, // convert to rial - 'OrderId' => crc32($this->invoice->getUuid()), - 'CallBackUrl' => $this->settings->callbackUrl, - 'AdditionalData' => $description, - ); + $phone = $this->invoice->getDetail('phone') + ?? $this->invoice->getDetail('cellphone') + ?? $this->invoice->getDetail('mobile'); + + + return [ + 'LoginAccount' => $this->settings->merchantId, + 'Amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'OrderId' => crc32($this->invoice->getUuid()), + 'CallBackUrl' => $this->settings->callbackUrl, + 'Originator' => $phone, + 'AdditionalData' => $description, + ]; } } diff --git a/src/Drivers/Pasargad/Pasargad.php b/src/Drivers/Pasargad/Pasargad.php index 3c19fa6..d7744f5 100644 --- a/src/Drivers/Pasargad/Pasargad.php +++ b/src/Drivers/Pasargad/Pasargad.php @@ -3,14 +3,16 @@ namespace Shetabit\Multipay\Drivers\Pasargad; use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Shetabit\Multipay\Drivers\Pasargad\PasargadHolder\PasargadHolder; use Shetabit\Multipay\Invoice; use Shetabit\Multipay\Receipt; use Shetabit\Multipay\Abstracts\Driver; use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Exceptions\InvalidPaymentException; -use Shetabit\Multipay\Drivers\Pasargad\Utils\RSAProcessor; use Shetabit\Multipay\RedirectionForm; -use Shetabit\Multipay\Request; +use DateTimeZone; +use DateTime; class Pasargad extends Driver { @@ -42,6 +44,12 @@ class Pasargad extends Driver */ protected $preparedData = array(); + /** + * Pasardad Holder + * @var string + */ + protected $holder; + /** * Pasargad(PEP) constructor. * Construct the class with the relevant settings. @@ -52,22 +60,38 @@ class Pasargad extends Driver public function __construct(Invoice $invoice, $settings) { $this->invoice($invoice); - $this->settings = (object) $settings; + $this->settings = (object)$settings; $this->client = new Client(); + $this->holder = new PasargadHolder(); } /** * Purchase Invoice. * * @return string + * @throws InvalidPaymentException + * @throws GuzzleException + * @throws \Exception */ - public function purchase() + public function purchase(): string { $invoiceData = $this->getPreparedInvoiceData(); - $this->invoice->transactionId($invoiceData['InvoiceNumber']); + $this->invoice->transactionId($invoiceData['invoice']); + + $response = $this->request( + $this->settings->apiPaymentUrl, + $invoiceData, + 'POST', + $this->createToken() + ); + + if ($response['data']['urlId']) { + $this->holder->urlId($response['data']['urlId']); + } else { + throw new InvalidPaymentException("urlId is not set."); + } - // return the transaction's id return $this->invoice->getTransactionId(); } @@ -76,14 +100,12 @@ public function purchase() * * @return RedirectionForm */ - public function pay() : RedirectionForm + public function pay(): RedirectionForm { - $paymentUrl = $this->settings->apiPaymentUrl; - $getTokenUrl = $this->settings->apiGetToken; - $tokenData = $this->request($getTokenUrl, $this->getPreparedInvoiceData()); + $paymentUrl = $this->settings->apiBaseUrl . $this->holder->getUrlId(); // redirect using HTML form - return $this->redirectWithForm($paymentUrl, $tokenData, 'POST'); + return $this->redirectWithForm($paymentUrl, ['Token' => $this->holder->getUrlId()]); } /** @@ -92,87 +114,79 @@ public function pay() : RedirectionForm * @return ReceiptInterface * * @throws InvalidPaymentException - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Exception + * @throws GuzzleException */ - public function verify() : ReceiptInterface + public function verify(): ReceiptInterface { - $invoiceDetails = $this->request( - $this->settings->apiCheckTransactionUrl, - [ - 'TransactionReferenceID' => Request::input('tref') - ] - ); - $fields = [ - 'MerchantCode' => $invoiceDetails['MerchantCode'], - 'TerminalCode' => $invoiceDetails['TerminalCode'], - 'InvoiceNumber' => $invoiceDetails['InvoiceNumber'], - 'InvoiceDate' => $invoiceDetails['InvoiceDate'], - 'Amount' => $invoiceDetails['Amount'], - 'Timestamp' => date("Y/m/d H:i:s"), - ]; + $transactionId = $this->invoice->getTransactionId(); - $verifyResult = $this->request($this->settings->apiVerificationUrl, $fields); + $payment_inquiry = $this->request( + $this->settings->paymentInquiry, + ['invoiceId' => $transactionId], + 'POST', + $this->createToken() + ); - return $this->createReceipt($verifyResult, $invoiceDetails); - } + if ($payment_inquiry['resultCode'] != 0) { + throw new InvalidPaymentException("This transaction is fail."); + } - /** - * Generate the payment's receipt - * - * @param $referenceId - * - * @return Receipt - */ - protected function createReceipt($verifyResult, $invoiceDetails) - { - $referenceId = $invoiceDetails['TransactionReferenceID']; - $traceNumber = $invoiceDetails['TraceNumber']; - $referenceNumber = $invoiceDetails['ReferenceNumber']; + if($payment_inquiry['data']['transactionId'] !== $transactionId) { + throw new InvalidPaymentException("This transaction is fail."); + } - $reciept = new Receipt('Pasargad', $referenceId); + $verifyResult = $this->request( + $this->settings->verifyPayment, + [ + 'invoice' => $transactionId, + 'urlId' => $payment_inquiry['data']['url'] + ], + 'POST', + $this->createToken() + ); - $reciept->detail('TraceNumber', $traceNumber); - $reciept->detail('ReferenceNumber', $referenceNumber); - $reciept->detail('MaskedCardNumber', $verifyResult['MaskedCardNumber']); - $reciept->detail('ShaparakRefNumber', $verifyResult['ShaparakRefNumber']); + if (!$verifyResult['data']['referenceNumber']) { + throw new InvalidPaymentException("This transaction is fail."); + } - return $reciept; + $receipt = $this->createReceipt($$verifyResult['data']['referenceNumber']); + + $receipt->detail([ + 'resultCode' => $verifyResult['resultCode'], + 'resultMsg' => $verifyResult['resultMsg'] ?? null, + 'hashedCardNumber' => $verifyResult['data']['hashedCardNumber'] ?? null, + 'maskedCardNumber' => $verifyResult['data']['maskedCardNumber'] ?? null, + 'invoiceId' => $transactionId, + 'referenceNumber' => $verifyResult['data']['referenceNumber'] ?? null, + 'trackId' => $verifyResult['data']['trackId'] ?? null, + 'amount' => $verifyResult['data']['amount'] ?? null, + 'requestDate' => $verifyResult['data']['requestDate'] ?? null, + ]); + + return $receipt; } - /** - * A default message for exceptions - * - * @return string - */ - protected function getDefaultExceptionMessage() - { - return 'مشکلی در دریافت اطلاعات از بانک به وجود آمده است'; - } /** - * Sign given data. - * - * @param string $data + * Generate the payment's receipt * - * @return string + * @param $referenceId + * @return Receipt */ - public function sign($data) + protected function createReceipt($referenceId): Receipt { - $certificate = $this->settings->certificate; - $certificateType = $this->settings->certificateType; - - $processor = new RSAProcessor($certificate, $certificateType); - - return $processor->sign($data); + return new Receipt('pasargad', $referenceId); } /** * Retrieve prepared invoice's data * * @return array + * @throws \Exception */ - protected function getPreparedInvoiceData() + protected function getPreparedInvoiceData(): array { if (empty($this->preparedData)) { $this->preparedData = $this->prepareInvoiceData(); @@ -185,57 +199,51 @@ protected function getPreparedInvoiceData() * Prepare invoice data * * @return array + * @throws \Exception */ protected function prepareInvoiceData(): array { - $action = 1003; // 1003 : for buy request (bank standard) - $merchantCode = $this->settings->merchantId; + $action = 8; // 8 : for buy request (bank standard) $terminalCode = $this->settings->terminalCode; - $amount = $this->invoice->getAmount(); //rial + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); $redirectAddress = $this->settings->callbackUrl; $invoiceNumber = crc32($this->invoice->getUuid()) . rand(0, time()); - $timeStamp = date("Y/m/d H:i:s"); - $invoiceDate = date("Y/m/d H:i:s"); + + $iranTime = new DateTime('now', new DateTimeZone('Asia/Tehran')); + $invoiceDate = $iranTime->format("Y/m/d H:i:s"); if (!empty($this->invoice->getDetails()['date'])) { $invoiceDate = $this->invoice->getDetails()['date']; } return [ - 'InvoiceNumber' => $invoiceNumber, - 'InvoiceDate' => $invoiceDate, - 'Amount' => $amount, - 'TerminalCode' => $terminalCode, - 'MerchantCode' => $merchantCode, - 'RedirectAddress' => $redirectAddress, - 'Timestamp' => $timeStamp, - 'Action' => $action, + 'invoice' => $invoiceNumber, + 'invoiceDate' => $invoiceDate, + 'amount' => $amount, + 'terminalNumber' => $terminalCode, + 'callbackApi' => $redirectAddress, + 'serviceCode' => $action, + 'nationalCode' => "", + 'serviceType' => "PURCHASE", + 'mobileNumber' => "" ]; } - /** - * Prepare signature based on Pasargad document - * - * @param string $data - * @return string - */ - public function prepareSignature(string $data): string - { - return base64_encode($this->sign(sha1($data, true))); - } - /** * Make request to pasargad's Api * * @param string $url * @param array $body * @param string $method + * @param string|null $token * @return array + * @throws GuzzleException + * @throws InvalidPaymentException */ - protected function request(string $url, array $body, $method = 'POST'): array + protected function request(string $url, array $body, string $method = 'POST', string $token = null): array { $body = json_encode($body); - $sign = $this->prepareSignature($body); + $token = $token !== null ? 'Bearer '.$token : null; $response = $this->client->request( 'POST', @@ -244,7 +252,7 @@ protected function request(string $url, array $body, $method = 'POST'): array 'body' => $body, 'headers' => [ 'content-type' => 'application/json', - 'Sign' => $sign + 'Authorization' => $token ], "http_errors" => false, ] @@ -252,10 +260,31 @@ protected function request(string $url, array $body, $method = 'POST'): array $result = json_decode($response->getBody(), true); - if ($result['IsSuccess'] === false) { - throw new InvalidPaymentException($result['Message']); + if ($result['resultMsg'] !== 'Successful') { + throw new InvalidPaymentException($result['resultMsg']); } return $result; } + + /** + * * make token with username and password + * @return string + * @throws InvalidPaymentException + */ + protected function createToken(): string + { + $data = [ + "username" => $this->settings->username, + "password" => $this->settings->password + ]; + + $getTokenUrl = $this->settings->apiGetToken; + + try { + return $this->request($getTokenUrl, $data)['token']; + } catch (GuzzleException|InvalidPaymentException $e) { + throw new InvalidPaymentException($e->getMessage()); + } + } } diff --git a/src/Drivers/Pasargad/PasargadHolder/PasargadHolder.php b/src/Drivers/Pasargad/PasargadHolder/PasargadHolder.php new file mode 100644 index 0000000..36d7d52 --- /dev/null +++ b/src/Drivers/Pasargad/PasargadHolder/PasargadHolder.php @@ -0,0 +1,22 @@ +urlId = $id; + return $this; + } + + + public function getUrlId(): string + { + return $this->urlId; + } +} diff --git a/src/Drivers/Pasargad/Utils/RSA.php b/src/Drivers/Pasargad/Utils/RSA.php deleted file mode 100644 index b52672c..0000000 --- a/src/Drivers/Pasargad/Utils/RSA.php +++ /dev/null @@ -1,143 +0,0 @@ -= 0; $i--) { - $digit = ord($data{$i}); - $part_res = bcmul($digit, $radix); - $result = bcadd($result, $part_res); - $radix = bcmul($radix, $base); - } - return $result; - } - - public static function numberToBinary($number, $blocksize) - { - $base = "256"; - $result = ""; - $div = $number; - while ($div > 0) { - $mod = bcmod($div, $base); - $div = bcdiv($div, $base); - $result = chr($mod) . $result; - } - return str_pad($result, $blocksize, "\x00", STR_PAD_LEFT); - } -} diff --git a/src/Drivers/Pasargad/Utils/RSAProcessor.php b/src/Drivers/Pasargad/Utils/RSAProcessor.php deleted file mode 100644 index 2824d0e..0000000 --- a/src/Drivers/Pasargad/Utils/RSAProcessor.php +++ /dev/null @@ -1,121 +0,0 @@ -modulus = RSA::binaryToNumber(base64_decode($xmlObject->Modulus)); - $this->publicKey = RSA::binaryToNumber(base64_decode($xmlObject->Exponent)); - $this->privateKey = RSA::binaryToNumber(base64_decode($xmlObject->D)); - $this->keyLength = strlen(base64_decode($xmlObject->Modulus)) * 8; - } - - /** - * Retrieve public key - * - * @return string|null - */ - public function getPublicKey() - { - return $this->publicKey; - } - - /** - * Retrieve private key - * - * @return string|null - */ - public function getPrivateKey() - { - return $this->privateKey; - } - - /** - * Retrieve key length - * - * @return integer - */ - public function getKeyLength() - { - return $this->keyLength; - } - - /** - * Retrieve modulus - * - * @return string|null - */ - public function getModulus() - { - return $this->modulus; - } - - /** - * Encrypt given data - * - * @param string $data - * - * @return string - */ - public function encrypt($data) - { - return base64_encode(RSA::rsaEncrypt($data, $this->publicKey, $this->modulus, $this->keyLength)); - } - - /** - * Decrypt given data - * - * @param $data - * - * @return string - */ - public function decrypt($data) - { - return RSA::rsaDecrypt($data, $this->privateKey, $this->modulus, $this->keyLength); - } - - /** - * Sign given data - * - * @param string $data - * - * @return string - */ - public function sign($data) - { - return RSA::rsaSign($data, $this->privateKey, $this->modulus, $this->keyLength); - } - - /** - * Verify RSA data - * - * @param string $data - * - * @return boolean - */ - public function verify($data) - { - return RSA::rsaVerify($data, $this->publicKey, $this->modulus, $this->keyLength); - } -} diff --git a/src/Drivers/Payfa/Payfa.php b/src/Drivers/Payfa/Payfa.php new file mode 100644 index 0000000..ea6385d --- /dev/null +++ b/src/Drivers/Payfa/Payfa.php @@ -0,0 +1,165 @@ +invoice($invoice); // Set the invoice. + $this->settings = (object)$settings; // Set settings. + $this->client = new Client(); + } + + /** + * Retrieve data from details using its name. + * + * @return string + */ + private function extractDetails($name) + { + return empty($this->invoice->getDetails()[$name]) ? null : $this->invoice->getDetails()[$name]; + } + + public function purchase() + { + $mobile = $this->extractDetails('mobile'); + $cardNumber = $this->extractDetails('cardNumber'); + + $data = array( + 'amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'callbackUrl' => $this->settings->callbackUrl, + 'mobileNumber' => $mobile, + 'invoiceId' => $this->invoice->getUuid(), + 'cardNumber' => $cardNumber + ); + + $response = $this->client->request( + 'POST', + $this->settings->apiPurchaseUrl, + [ + "json" => $data, + "http_errors" => false, + "headers" => [ + "X-API-Key" => $this->settings->apiKey, + 'Content-Type' => 'application/json', + ] + ] + ); + $body = json_decode($response->getBody()->getContents(), true); + + + if ($response->getStatusCode() != 200) { + throw new PurchaseFailedException($body["title"]); + } + + $this->invoice->transactionId($body['paymentId']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + $payUrl = $this->settings->apiPaymentUrl . $this->invoice->getTransactionId(); + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verify(): ReceiptInterface + { + $paymentId = $this->invoice->getTransactionId() ?? Request::input('paymentId'); + + + $response = $this->client->request( + 'POST', + $this->settings->apiVerificationUrl . $paymentId, + [ + "http_errors" => false, + "headers" => [ + "X-API-Key" => $this->settings->apiKey, + 'Content-Type' => 'application/json', + ] + ] + ); + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200) { + $this->notVerified($body["message"], $response->getStatusCode()); + } + + return $this->createReceipt($body['transactionId']); + } + + protected function createReceipt($referenceId) + { + return new Receipt('payfa', $referenceId); + } + + /** + * Trigger an exception + * + * @param $message + * @throws InvalidPaymentException + */ + private function notVerified($message, $status) + { + if (empty($message)) { + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); + } else { + throw new InvalidPaymentException($message, (int)$status); + } + } +} diff --git a/src/Drivers/Payir/Payir.php b/src/Drivers/Payir/Payir.php index ab27a7d..719f22a 100644 --- a/src/Drivers/Payir/Payir.php +++ b/src/Drivers/Payir/Payir.php @@ -75,7 +75,7 @@ public function purchase() $data = array( 'api' => $this->settings->merchantId, - 'amount' => ($this->invoice->getAmount() * 10), // convert rial to toman + 'amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial 'redirect' => $this->settings->callbackUrl, 'mobile' => $mobile, 'description' => $description, @@ -109,9 +109,9 @@ public function purchase() * * @return RedirectionForm */ - public function pay() : RedirectionForm + public function pay(): RedirectionForm { - $payUrl = $this->settings->apiPaymentUrl.$this->invoice->getTransactionId(); + $payUrl = $this->settings->apiPaymentUrl . $this->invoice->getTransactionId(); return $this->redirectWithForm($payUrl, [], 'GET'); } @@ -124,7 +124,7 @@ public function pay() : RedirectionForm * @throws InvalidPaymentException * @throws \GuzzleHttp\Exception\GuzzleException */ - public function verify() : ReceiptInterface + public function verify(): ReceiptInterface { $data = [ 'api' => $this->settings->merchantId, @@ -141,8 +141,12 @@ public function verify() : ReceiptInterface ); $body = json_decode($response->getBody()->getContents(), true); - if ($body['status'] != 1) { - $this->notVerified($body['errorCode']); + if (isset($body['status'])) { + if ($body['status'] != 1) { + $this->notVerified($body['errorCode']); + } + } else { + $this->notVerified(null); } return $this->createReceipt($body['transId']); @@ -172,17 +176,39 @@ protected function createReceipt($referenceId) private function notVerified($status) { $translations = array( - "-1" => "ارسال api الزامی می باشد", - "-2" => "ارسال transId الزامی می باشد", - "-3" => "درگاه پرداختی با api ارسالی یافت نشد و یا غیر فعال می باشد", - "-4" => "فروشنده غیر فعال می باشد", - "-5" => "تراکنش با خطا مواجه شده است", + 0 => 'درحال حاضر درگاه بانکی قطع شده و مشکل بزودی برطرف می شود', + -1 => 'API Key ارسال نمی شود', + -2 => 'Token ارسال نمی شود', + -3 => 'API Key ارسال شده اشتباه است', + -4 => 'امکان انجام تراکنش برای این پذیرنده وجود ندارد', + -5 => 'تراکنش با خطا مواجه شده است', + -6 => 'تراکنش تکراریست یا قبلا انجام شده', + -7 => 'مقدار Token ارسالی اشتباه است', + -8 => 'شماره تراکنش ارسالی اشتباه است', + -9 => 'زمان مجاز برای انجام تراکنش تمام شده', + -10 => 'مبلغ تراکنش ارسال نمی شود', + -11 => 'مبلغ تراکنش باید به صورت عددی و با کاراکترهای لاتین باشد', + -12 => 'مبلغ تراکنش می بایست عددی بین 10,000 و 500,000,000 ریال باشد', + -13 => 'مقدار آدرس بازگشتی ارسال نمی شود', + -14 => 'آدرس بازگشتی ارسالی با آدرس درگاه ثبت شده در شبکه پرداخت پی یکسان نیست', + -15 => 'امکان وریفای وجود ندارد. این تراکنش پرداخت نشده است', + -16 => 'یک یا چند شماره موبایل از اطلاعات پذیرندگان ارسال شده اشتباه است', + -17 => 'میزان سهم ارسالی باید بصورت عددی و بین 1 تا 100 باشد', + -18 => 'فرمت پذیرندگان صحیح نمی باشد', + -19 => 'هر پذیرنده فقط یک سهم میتواند داشته باشد', + -20 => 'مجموع سهم پذیرنده ها باید 100 درصد باشد', + -21 => 'Reseller ID ارسالی اشتباه است', + -22 => 'فرمت یا طول مقادیر ارسالی به درگاه اشتباه است', + -23 => 'سوییچ PSP ( درگاه بانک ) قادر به پردازش درخواست نیست. لطفا لحظاتی بعد مجددا تلاش کنید', + -24 => 'شماره کارت باید بصورت 16 رقمی، لاتین و چسبیده بهم باشد', + -25 => 'امکان استفاده از سرویس در کشور مبدا شما وجود نداره', + -26 => 'امکان انجام تراکنش برای این درگاه وجود ندارد', ); if (array_key_exists($status, $translations)) { - throw new InvalidPaymentException($translations[$status]); + throw new InvalidPaymentException($translations[$status], (int)$status); } else { - throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.'); + throw new InvalidPaymentException('تراکنش با خطا مواجه شد.', (int)$status); } } } diff --git a/src/Drivers/Paypal/Paypal.php b/src/Drivers/Paypal/Paypal.php index 4169c4b..cfc5180 100644 --- a/src/Drivers/Paypal/Paypal.php +++ b/src/Drivers/Paypal/Paypal.php @@ -126,7 +126,7 @@ public function verify() : ReceiptInterface if ($result->Status != 100) { $message = $this->translateStatus($result->Status); - throw new InvalidPaymentException($message); + throw new InvalidPaymentException($message, (int)$result->Status); } return $this->createReceipt($result->RefID); diff --git a/src/Drivers/Payping/Payping.php b/src/Drivers/Payping/Payping.php index 91ecfa7..aa01bdb 100755 --- a/src/Drivers/Payping/Payping.php +++ b/src/Drivers/Payping/Payping.php @@ -76,7 +76,7 @@ public function purchase() $data = array( "payerName" => $name, - "amount" => $this->invoice->getAmount(), + "amount" => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman "payerIdentity" => $mobile ?? $email, "returnUrl" => $this->settings->callbackUrl, "description" => $description, @@ -139,7 +139,7 @@ public function verify() : ReceiptInterface { $refId = Request::input('refid'); $data = [ - 'amount' => $this->invoice->getAmount(), + 'amount' => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman 'refId' => $refId, ]; @@ -164,10 +164,17 @@ public function verify() : ReceiptInterface if ($statusCode !== 200) { $message = is_array($body) ? array_pop($body) : $this->convertStatusCodeToMessage($statusCode); - $this->notVerified($message); + $this->notVerified($message, $statusCode); } - return $this->createReceipt($refId); + $receipt = $this->createReceipt($refId); + + $receipt->detail([ + "cardNumber" => $body['cardnumber'], + ]); + + + return $receipt; } /** @@ -191,9 +198,9 @@ protected function createReceipt($referenceId) * * @throws InvalidPaymentException */ - private function notVerified($message) + private function notVerified($message, $status) { - throw new InvalidPaymentException($message); + throw new InvalidPaymentException($message, (int)$status); } /** diff --git a/src/Drivers/Paystar/Paystar.php b/src/Drivers/Paystar/Paystar.php index 476e3da..9bf2c18 100644 --- a/src/Drivers/Paystar/Paystar.php +++ b/src/Drivers/Paystar/Paystar.php @@ -35,6 +35,13 @@ class Paystar extends Driver */ protected $settings; + /** + * payment token + * + * @var $token + */ + protected $token; + /** * Paystar constructor. * Construct the class with the relevant settings. @@ -60,15 +67,24 @@ public function __construct(Invoice $invoice, $settings) public function purchase() { $details = $this->invoice->getDetails(); + $order_id = $this->invoice->getUuid(); + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + $callback = $this->settings->callbackUrl; - $data = array( - 'amount' => $this->invoice->getAmount(), - 'email' => $details['email'] ?? null, + $data = [ + 'amount' => $amount, + 'order_id' => $order_id, + 'mail' => $details['email'] ?? null, 'phone' => $details['mobile'] ?? $details['phone'] ?? null, - 'pin' => $this->settings->merchantId, - 'desc' => $details['description'] ?? $this->settings->description, - 'callback' => $this->settings->callbackUrl, - ); + 'description' => $details['description'] ?? $this->settings->description, + 'callback' => $callback, + 'sign' => + hash_hmac( + 'SHA512', + $amount . '#' . $order_id . '#' . $callback, + $this->settings->signKey + ), + ]; $response = $this ->client @@ -76,19 +92,24 @@ public function purchase() 'POST', $this->settings->apiPurchaseUrl, [ - "form_params" => $data, - "http_errors" => false, + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $this->settings->gatewayId, + ], + 'body' => json_encode($data), ] ); - $body = $response->getBody()->getContents(); + $body = json_decode($response->getBody()->getContents()); - if (is_numeric($body)) { + if ($body->status !== 1) { // some error has happened - throw new PurchaseFailedException($this->translateStatus($body)); + throw new PurchaseFailedException($this->translateStatus($body->status)); } - $this->invoice->transactionId($body); + $this->invoice->transactionId($body->data->ref_num); + $this->token = $body->data->token; // return the transaction's id return $this->invoice->getTransactionId(); @@ -101,10 +122,13 @@ public function purchase() */ public function pay() : RedirectionForm { - $apiUrl = $this->settings->apiPaymentUrl; - $payUrl = $apiUrl.$this->invoice->getTransactionId(); - - return $this->redirectWithForm($payUrl, [], 'GET'); + return $this->redirectWithForm( + $this->settings->apiPaymentUrl, + [ + 'token' => $this->token, + ], + 'POST' + ); } /** @@ -117,29 +141,47 @@ public function pay() : RedirectionForm */ public function verify() : ReceiptInterface { - $transId = $this->invoice->getTransactionId() ?? Request::input('transid'); + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + $refNum = Request::post('ref_num'); + $cardNumber = Request::post('card_number'); + $trackingCode = Request::post('tracking_code'); + + if (!$trackingCode) { + throw new InvalidPaymentException($this->translateStatus(-1), -1); + } $data = [ - 'amount' => $this->invoice->getAmount(), - 'pin' => $this->settings->merchantId, - 'transid' => $transId, + 'amount' => $amount, + 'ref_num' => $refNum, + 'tracking_code' => $trackingCode, + 'sign' => + hash_hmac( + 'SHA512', + $amount . '#' . $refNum . '#' . $cardNumber . '#' . $trackingCode, + $this->settings->signKey + ), ]; $response = $this->client->request( 'POST', $this->settings->apiVerificationUrl, [ - 'form_params' => $data, - "http_errors" => false, + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $this->settings->gatewayId, + ], + 'body' => json_encode($data), ] ); - $body = $response->getBody()->getContents(); - if ($body != 1) { - throw new InvalidPaymentException($this->translateStatus($body)); + $body = json_decode($response->getBody()->getContents()); + + if ($body->status !== 1) { + throw new InvalidPaymentException($this->translateStatus($body->status), (int)$body->status); } - return $this->createReceipt($transId); + return $this->createReceipt($refNum); } /** @@ -167,22 +209,20 @@ private function translateStatus($status) { $status = (string) $status; - $translations = array( - "−1" => "مبلغ پرداخت نمیتواند خالی باشد.", - "−2" => "کد پین درگاه(کد مرچند) نمیتواند خالی باشد.", - "−3" => "لینک برگشتی (callback) نمیتواند خالی باشد.", - "−4" => "مبلغ پرداخت باید عددی باشد.", - "−5" => "مبلغ پرداخت باید بزرگتر از ۱۰۰ باشد.", - "−6" => "کد پین درگاه (مرچند) اشتباه است.", - "−7" => "آیپی سرور با آیپی درگاه مطابقت ندارد", - "−8" => "کد تراکنش (transid) نمیتواند خالی باشد.", - "−9" => "تراکنش مورد نظر وجود ندارد.", - "−10" => "کدپین درگاه با درگاه تراکنش مطابقت ندارد.", - "−11" => "مبلغ با مبلغ تراکنش مطابقت ندارد.", - "-12" => "بانک انتخابی اشتباه است.", - "-13" => "درگاه غیرفعال است.", - "-14" => "آیپی مشتری ارسال نشده است.", - ); + $translations = [ + '1' => 'موفق', + '-1' => 'درخواست نامعتبر (خطا در پارامترهای ورودی)', + '-2' => 'درگاه فعال نیست', + '-3' => 'توکن تکراری است', + '-4' => 'مبلغ بیشتر از سقف مجاز درگاه است', + '-5' => 'شناسه ref_num معتبر نیست', + '-6' => 'تراکنش قبلا وریفای شده است', + '-7' => 'پارامترهای ارسال شده نامعتبر است', + '-8' => 'تراکنش را نمیتوان وریفای کرد', + '-9' => 'تراکنش وریفای نشد', + '-98' => 'تراکنش ناموفق', + '-99' => 'خطای سامانه' + ]; $unknownError = 'خطای ناشناخته رخ داده است.'; diff --git a/src/Drivers/Poolam/Poolam.php b/src/Drivers/Poolam/Poolam.php index fdea4ec..1e5cc62 100644 --- a/src/Drivers/Poolam/Poolam.php +++ b/src/Drivers/Poolam/Poolam.php @@ -58,12 +58,11 @@ public function __construct(Invoice $invoice, $settings) */ public function purchase() { - // convert to toman - $toman = $this->invoice->getAmount() * 10; + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial $data = array( 'api_key' => $this->settings->merchantId, - 'amount' => $toman, + 'amount' => $amount, 'return_url' => $this->settings->callbackUrl, ); @@ -127,7 +126,7 @@ public function verify() : ReceiptInterface if (empty($body['status']) || $body['status'] != 1) { $message = $body['errorDescription'] ?? null; - $this->notVerified($message); + $this->notVerified($message, $body['status']); } return $this->createReceipt($body['bank_code']); @@ -153,12 +152,12 @@ protected function createReceipt($referenceId) * @param $message * @throws InvalidPaymentException */ - private function notVerified($message) + private function notVerified($message, $status) { if (empty($message)) { - throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.'); + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); } else { - throw new InvalidPaymentException($message); + throw new InvalidPaymentException($message, (int)$status); } } } diff --git a/src/Drivers/Rayanpay/Rayanpay.php b/src/Drivers/Rayanpay/Rayanpay.php new file mode 100644 index 0000000..e58002c --- /dev/null +++ b/src/Drivers/Rayanpay/Rayanpay.php @@ -0,0 +1,325 @@ +invoice($invoice); + $this->settings = (object)$settings; + $this->client = new Client( + [ + 'base_uri' => $this->settings->apiPurchaseUrl, + 'verify' => false + ] + ); + } + + /** + * @throws InvalidPaymentException + */ + private function auth() + { + $data = [ + 'clientId' => $this->settings->client_id, + 'userName' => $this->settings->username, + 'password' => $this->settings->password, + ]; + return $this->makeHttpChargeRequest( + $data, + $this->settings->apiTokenUrl, + 'token', + false + ); + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function purchase() + { + $this->auth(); + + $details = $this->invoice->getDetails(); + + if (!empty($details['mobile'])) { + $mobile = $details['mobile']; + } + if (!empty($details['phone'])) { + $mobile = $details['phone']; + } + + if (empty($mobile)) { + throw new PurchaseFailedException('شماره موبایل را وارد کنید.'); + } + + if (preg_match('/^(?:98)?9[0-9]{9}$/', $mobile) == false) { + $mobile = ''; + } + + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + + if ($amount <= 10000) { + throw new PurchaseFailedException('مقدار مبلغ ارسالی بزگتر از 10000 ریال باشد.'); + } + + $referenceId = hexdec(uniqid()); + + $callback = $this->settings->callbackUrl . "?referenceId=" . $referenceId . "&price=" . $amount . "&mobile=" . $mobile; + + $data = [ + 'referenceId' => $referenceId, + 'amount' => $amount, + 'msisdn' => $mobile, + 'gatewayId' => 100, + 'callbackUrl' => $callback, + 'gateSwitchingAllowed' => true, + ]; + + $response = $this->makeHttpChargeRequest( + $data, + $this->settings->apiPayStart, + 'payment_start', + true + ); + + $body = json_decode($response, true); + + $this->invoice->transactionId($referenceId); + + // Get RefIf From Html Form Becuese GetWay Not Provide In Api + $dom = new \DOMDocument(); + $dom->loadHTML($body['bankRedirectHtml']); + $xp = new \DOMXPath($dom); + $nodes = $xp->query('//input[@name="RefId"]'); + $node = $nodes->item(0); + $_SESSION['RefId'] = $node->getAttribute('value'); + + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice render html redirect to getway + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + return $this->redirectWithForm($this->settings->apiPurchaseUrl, [ + 'x_GateChanged' => 0, + 'RefId' => !empty($_SESSION['RefId']) ? $_SESSION['RefId'] : null, + ], 'POST'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verify(): ReceiptInterface + { + $data = [ + 'referenceId' => (int)$this->getInvoice()->getTransactionId(), + 'header' => '', + 'content' => http_build_query($_POST), + ]; + + $response = $this->makeHttpChargeRequest( + $data, + $this->settings->apiPayVerify, + 'payment_parse', + true + ); + + $body = json_decode($response, true); + + $receipt = $this->createReceipt($body['paymentId']); + + $receipt->detail([ + 'paymentId' => $body['paymentId'], + 'hashedBankCardNumber' => $body['hashedBankCardNumber'], + 'endDate' => $body['endDate'], + ]); + + return $receipt; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId) + { + $receipt = new Receipt('rayanpay', $referenceId); + + return $receipt; + } + + + /** + * Trigger an exception + * + * @param $status + * @param $method + * @throws InvalidPaymentException + */ + private function notVerified($status, $method) + { + $message = ""; + if ($method == 'token') { + switch ($status) { + case '400': + $message = 'نقص در پارامترهای ارسالی'; + break; + + case '401': + $message = 'کد کاربری/رمز عبور /کلاینت/آی پی نامعتبر است'; + break; + + case '500': + $message = 'خطایی سمت سرور رخ داده است'; + break; + } + } elseif ($method == 'payment_start') { + switch ($status) { + case '400': + $message = 'شناسه ارسالی تکراری می باشد '; + break; + case '401': + $message = 'توکن نامعتبر'; + break; + + case '601': + $message = 'اتصال به درگاه خطا دارد (پرداخت ناموفق)'; + break; + + case '500': + $message = 'خطایی سمت سرور رخ داده است (احتمال تکراری بودن شماره ref شما یا اگر شماره موبایل دارید باید فرمت زیر باشد 989121112233 )'; + break; + } + } elseif ($method == 'payment_status') { + switch ($status) { + case '401': + $message = 'توکن نامعتبر است'; + break; + case '601': + $message = 'پرداخت ناموفق'; + break; + + case '600': + $message = 'پرداخت در حالت Pending می باشد و باید متد fullfill برای تعیین وضعیت صدا زده شود'; + break; + } + } elseif ($method == 'payment_parse') { + switch ($status) { + case '401': + $message = 'توکن نامعتبر است'; + break; + + case '500': + $message = 'خطایی سمت سرور رخ داده است'; + break; + + case '600': + $message = 'وضعیت نامشخص'; + break; + + case '601': + $message = 'پرداخت ناموفق'; + break; + + case '602': + $message = 'پرداخت یافت نشد'; + break; + + case '608': + $message = 'قوانین پرداخت یافت نشد (برای پرداخت هایی که قوانین دارند)'; + break; + + case '609': + $message = 'وضعیت پرداخت نامعتبر میباشد'; + break; + } + } + if ($message) { + throw new InvalidPaymentException($message, (int)$status); + } else { + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); + } + } + + private function makeHttpChargeRequest($data, $url, $method, $forAuth = true) + { + $header[] = 'Content-Type: application/json'; + if ($forAuth) { + $header[] = 'Authorization: Bearer ' . $this->auth(); + } + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $result = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($http_code != 200) { + return $this->notVerified($http_code, $method); + } + return $result; + } +} diff --git a/src/Drivers/SEP/SEP.php b/src/Drivers/SEP/SEP.php new file mode 100644 index 0000000..c046c83 --- /dev/null +++ b/src/Drivers/SEP/SEP.php @@ -0,0 +1,241 @@ +invoice($invoice); + $this->settings = (object)$settings; + $this->client = new Client([ + 'curl' => [CURLOPT_SSL_CIPHER_LIST => 'DEFAULT@SECLEVEL=1'], + ]); + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + $data = array( + 'action' => 'token', + 'TerminalId' => $this->settings->terminalId, + 'Amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'ResNum' => $this->invoice->getUuid(), + 'RedirectUrl' => $this->settings->callbackUrl, + 'CellNumber' => $this->invoice->getDetail('mobile') ?? '', + 'ResNum1' => $this->invoice->getDetail('ResNum1') ?? '', + 'ResNum2' => $this->invoice->getDetail('ResNum2') ?? '', + 'ResNum3' => $this->invoice->getDetail('ResNum3') ?? '', + 'ResNum4' => $this->invoice->getDetail('ResNum4') ?? '', + ); + + $response = $this->client->post( + $this->settings->apiGetToken, + [ + 'json' => $data, + ] + ); + + $responseStatus = $response->getStatusCode(); + + if ($responseStatus != 200) { // if something has done in a wrong way + $this->purchaseFailed(0); + } + + $jsonBody = $response->getBody()->getContents(); + $responseData = json_decode($jsonBody, true); + + if ($responseData['status'] != 1) { + $this->purchaseFailed($responseData['errorCode']); + } + + // set transaction id + $this->invoice->transactionId($responseData['token']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + $payUrl = $this->settings->apiPaymentUrl; + + return $this->redirectWithForm( + $payUrl, + [ + 'Token' => $this->invoice->getTransactionId(), + 'GetMethod' => false, + ] + ); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \SoapFault + * @throws PurchaseFailedException + */ + public function verify(): ReceiptInterface + { + $status = (int)Request::input('Status'); + if ($status != 2) { + $this->purchaseFailed($status); + } + + $data = array( + 'RefNum' => Request::input('RefNum'), + 'TerminalNumber' => $this->settings->terminalId, + ); + + $response = $this->client->post( + $this->settings->apiVerificationUrl, + [ + 'json' => $data, + ] + ); + + if ($response->getStatusCode() != 200) { + $this->notVerified(0); + } + + $jsonData = $response->getBody()->getContents(); + $responseData = json_decode($jsonData, true); + + if ($responseData['ResultCode'] != 0) { + $this->notVerified($responseData['ResultCode']); + } + + $transactionDetail = $responseData['TransactionDetail']; + + $receipt = $this->createReceipt($data['RefNum']); + $receipt->detail([ + 'traceNo' => $transactionDetail['StraceNo'], + 'referenceNo' => $transactionDetail['RRN'], + 'transactionId' => $transactionDetail['RefNum'], + 'cardNo' => $transactionDetail['MaskedPan'], + ]); + + return $receipt; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId) + { + $receipt = new Receipt('saman', $referenceId); + + return $receipt; + } + + /** + * Trigger an exception + * + * @param $status + * + * @throws PurchaseFailedException + */ + protected function purchaseFailed($status) + { + $translations = array( + 1 => ' تراکنش توسط خریدار لغو شده است.', + 2 => 'پرداخت با موفقیت انجام شد.', + 3 => 'پرداخت انجام نشد.', + 4 => 'کاربر در بازه زمانی تعیین شده پاسخی ارسال نکرده است.', + 5 => 'پارامترهای ارسالی نامعتبر است.', + 8 => 'آدرس سرور پذیرنده نامعتبر است.', + 9 => 'رمز کارت 3 مرتبه اشتباه وارد شده است در نتیجه کارت غیر فعال خواهد شد.', + 10 => 'توکن ارسال شده یافت نشد.', + 11 => 'با این شماره ترمینال فقط تراکنش های توکنی قابل پرداخت هستند.', + 12 => 'شماره ترمینال ارسال شده یافت نشد.', + ); + + if (array_key_exists($status, $translations)) { + throw new PurchaseFailedException($translations[$status]); + } else { + throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); + } + } + + /** + * Trigger an exception + * + * @param $status + * + * @throws InvalidPaymentException + */ + private function notVerified($status) + { + $translations = array( + -2 => ' تراکنش یافت نشد.', + -6 => 'بیش از 30 دقیقه از زمان اجرای تراکنش گذشته است.', + 2 => 'کاربر در بازه زمانی تعیین شده پاسخی ارسال نکرده است.', + -104 => 'پارامترهای ارسالی نامعتبر است.', + -105 => 'آدرس سرور پذیرنده نامعتبر است.', + -106 => 'رمز کارت 3 مرتبه اشتباه وارد شده است در نتیجه کارت غیر فعال خواهد شد.', + ); + + if (array_key_exists($status, $translations)) { + throw new InvalidPaymentException($translations[$status], (int)$status); + } else { + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); + } + } +} diff --git a/src/Drivers/Sadad/Sadad.php b/src/Drivers/Sadad/Sadad.php index c53a771..5bf6e31 100644 --- a/src/Drivers/Sadad/Sadad.php +++ b/src/Drivers/Sadad/Sadad.php @@ -11,6 +11,8 @@ use Shetabit\Multipay\Receipt; use Shetabit\Multipay\RedirectionForm; use Shetabit\Multipay\Request; +use DateTimeZone; +use DateTime; class Sadad extends Driver { @@ -61,36 +63,82 @@ public function purchase() { $terminalId = $this->settings->terminalId; $orderId = crc32($this->invoice->getUuid()); - $amount = $this->invoice->getAmount() * 10; // convert to rial + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial $key = $this->settings->key; $signData = $this->encrypt_pkcs7("$terminalId;$orderId;$amount", $key); + $iranTime = new DateTime('now', new DateTimeZone('Asia/Tehran')); - $data = array( + //set Description for payment + if (!empty($this->invoice->getDetails()['description'])) { + $description = $this->invoice->getDetails()['description']; + } else { + $description = $this->settings->description; + } + + //set MobileNo for get user cards + if (!empty($this->invoice->getDetails()['mobile'])) { + $mobile = $this->invoice->getDetails()['mobile']; + } else { + $mobile = ""; + } + + $data = [ 'MerchantId' => $this->settings->merchantId, 'ReturnUrl' => $this->settings->callbackUrl, - 'LocalDateTime' => date("m/d/Y g:i:s a"), + 'LocalDateTime' => $iranTime->format("m/d/Y g:i:s a"), 'SignData' => $signData, 'TerminalId' => $terminalId, 'Amount' => $amount, 'OrderId' => $orderId, - ); + 'additionalData' => $description, + 'UserId' => $mobile, + ]; + + $mode = $this->getMode(); + + if ($mode == 'paymentbyidentity') { + //set PaymentIdentity for payment + if (!empty($this->invoice->getDetails()['payment_identity'])) { + $data['PaymentIdentity'] = $this->invoice->getDetails()['payment_identity']; + } else { + $data['PaymentIdentity'] = $this->settings->PaymentIdentity; + } + } elseif ($mode == 'paymentbymultiidentity') { + //set MultiIdentityData for payment + if (!empty($this->invoice->getDetails()['multi_identity_rows'])) { + $multiIdentityRows = $this->invoice->getDetails()['multi_identity_rows']; + } else { + $multiIdentityRows = $this->settings->MultiIdentityRows; + } + + // convert to rial + if ($this->settings->currency == 'T') { + $multiIdentityRows = array_map(function ($item) { + $item['Amount'] = $item['Amount'] * 10; + return $item; + }, $multiIdentityRows); + } + + $data['MultiIdentityData'] = ['MultiIdentityRows' => $multiIdentityRows]; + } $response = $this ->client ->request( 'POST', - $this->settings->apiPurchaseUrl, + $this->getPaymentUrl(), [ "json" => $data, "headers" => [ 'Content-Type' => 'application/json', + 'User-Agent' => '', ], "http_errors" => false, ] ); - $body = @json_decode($response->getBody()->getContents(), true); + $body = @json_decode($response->getBody()->getContents()); if (empty($body)) { throw new PurchaseFailedException('دسترسی به صفحه مورد نظر امکان پذیر نمی باشد.'); @@ -112,9 +160,9 @@ public function purchase() public function pay() : RedirectionForm { $token = $this->invoice->getTransactionId(); - $payUrl = $this->settings->apiPaymentUrl.'?Token='.$token; + $payUrl = $this->getPurchaseUrl(); - return $this->redirectWithForm($payUrl, [], 'GET'); + return $this->redirectWithForm($payUrl, ['Token' => $token], 'GET'); } /** @@ -130,10 +178,9 @@ public function verify() : ReceiptInterface $key = $this->settings->key; $token = $this->invoice->getTransactionId() ?? Request::input('token'); $resCode = Request::input('ResCode'); - $message = 'تراکنش نا موفق بود در صورت کسر مبلغ از حساب شما حداکثر پس از 72 ساعت مبلغ به حسابتان برمیگردد.'; - if ($resCode == 0) { - throw new InvalidPaymentException($message); + if ($resCode != 0) { + throw new InvalidPaymentException($this->translateStatus($resCode), $resCode); } $data = array( @@ -145,20 +192,22 @@ public function verify() : ReceiptInterface ->client ->request( 'POST', - $this->settings->apiPurchaseUrl, + $this->settings->apiVerificationUrl, [ "json" => $data, "headers" => [ 'Content-Type' => 'application/json', + 'User-Agent' => '', ], "http_errors" => false, ] ); - $body = json_decode($response->getBody()->getContents(), true); + $body = json_decode($response->getBody()->getContents()); - if ($body->ResCode == -1) { - throw new InvalidPaymentException($message); + $bodyResponse = $body->ResCode; + if ($bodyResponse != 0) { + throw new InvalidPaymentException($this->translateStatus($bodyResponse), $bodyResponse); } /** @@ -167,7 +216,15 @@ public function verify() : ReceiptInterface * شماره مرجع : $body->RetrievalRefNo */ - return $this->createReceipt($body->SystemTraceNo); + $receipt = $this->createReceipt($body->SystemTraceNo); + $receipt->detail([ + 'orderId' => $body->OrderId, + 'traceNo' => $body->SystemTraceNo, + 'referenceNo' => $body->RetrivalRefNo, + 'description' => $body->Description, + ]); + + return $receipt; } /** @@ -199,4 +256,113 @@ protected function encrypt_pkcs7($str, $key) return base64_encode($ciphertext); } + + /** + * Retrieve payment mode. + * + * @return string + */ + protected function getMode() : string + { + return strtolower($this->settings->mode); + } + + + /** + * Retrieve purchase url + * + * @return string + */ + protected function getPurchaseUrl() : string + { + return $this->settings->apiPurchaseUrl; + } + + /** + * Retrieve Payment url + * + * @return string + */ + protected function getPaymentUrl() : string + { + $mode = $this->getMode(); + + switch ($mode) { + case 'paymentbyidentity': + $url = $this->settings->apiPaymentByIdentityUrl; + break; + case 'paymentbymultiidentity': + $url = $this->settings->apiPaymentByMultiIdentityUrl; + break; + default: // default: normal + $url = $this->settings->apiPaymentUrl; + break; + } + + return $url; + } + + /** + * Convert status to a readable message. + * + * @param $status + * + * @return mixed|string + */ + private function translateStatus($status) + { + $translations = [ + '0' => 'تراکنش با موفقیت انجام شد', + '3' => 'پذيرنده کارت فعال نیست لطفا با بخش امور پذيرندگان، تماس حاصل فرمائید', + '23' => 'پذيرنده کارت نا معتبر لطفا با بخش امور پذيرندگان، تماس حاصل فرمائید', + '58' => 'انجام تراکنش مربوطه توسط پايانه ی انجام دهنده مجاز نمی باشد', + '61' => 'مبلغ تراکنش از حد مجاز بالاتر است', + '101' => 'مهلت ارسال تراکنش به پايان رسیده است', + '1000' => 'ترتیب پارامترهای ارسالی اشتباه می باشد', + '1001' => 'پارامترهای پرداخت اشتباه می باشد', + '1002' => 'خطا در سیستم- تراکنش ناموفق', + '1003' => 'IP پذيرنده اشتباه است', + '1004' => 'شماره پذيرنده اشتباه است', + '1005' => 'خطای دسترسی:لطفا بعدا تلاش فرمايید', + '1006' => 'خطا در سیستم', + '1011' => 'درخواست تکراری- شماره سفارش تکراری می باشد', + '1012' => 'اطلاعات پذيرنده صحیح نیست، يکی از موارد تاريخ،زمان يا کلید تراکنش اشتباه است', + '1015' => 'پاسخ خطای نامشخص از سمت مرکز', + '1017' => 'مبلغ درخواستی شما جهت پرداخت از حد مجاز تعريف شده برای اين پذيرنده بیشتر است', + '1018' => 'اشکال در تاريخ و زمان سیستم. لطفا تاريخ و زمان سرور خود را با بانک هماهنگ نمايید', + '1019' => 'امکان پرداخت از طريق سیستم شتاب برای اين پذيرنده امکان پذير نیست', + '1020' => 'پذيرنده غیرفعال شده است', + '1023' => 'آدرس بازگشت پذيرنده نامعتبر است', + '1024' => 'مهر زمانی پذيرنده نامعتبر است', + '1025' => 'امضا تراکنش نامعتبر است', + '1026' => 'شماره سفارش تراکنش نامعتبر است', + '1027' => 'شماره پذيرنده نامعتبر است', + '1028' => 'شماره ترمینال پذيرنده نامعتبر است', + '1029' => 'آدرس IP پرداخت در محدوده آدرس های معتبر اعلام شده توسط پذيرنده نیست', + '1030' => 'آدرس Domain پرداخت در محدوده آدرس های معتبر اعلام شده توسط پذیرنده نیست', + '1031' => 'مهلت زمانی شما جهت پرداخت به پايان رسیده است.لطفا مجددا سعی بفرمایید', + '1032' => 'پرداخت با اين کارت , برای پذيرنده مورد نظر شما امکان پذير نیست', + '1033' => 'به علت مشکل در سايت پذيرنده, پرداخت برای اين پذيرنده غیرفعال شده است', + '1036' => 'اطلاعات اضافی ارسال نشده يا دارای اشکال است', + '1037' => 'شماره پذيرنده يا شماره ترمینال پذيرنده صحیح نمیباشد', + '1053' => 'خطا: درخواست معتبر، از سمت پذيرنده صورت نگرفته است لطفا اطلاعات پذيرنده خود را چک کنید', + '1055' => 'مقدار غیرمجاز در ورود اطلاعات', + '1056' => 'سیستم موقتا قطع میباشد.لطفا بعدا تلاش فرمايید', + '1058' => 'سرويس پرداخت اينترنتی خارج از سرويس می باشد.لطفا بعدا سعی بفرمایید', + '1061' => 'اشکال در تولید کد يکتا. لطفا مرورگر خود را بسته و با اجرای مجدد عملیات پرداخت را انجام دهید', + '1064' => 'لطفا مجددا سعی بفرمايید', + '1065' => 'ارتباط ناموفق .لطفا چند لحظه ديگر مجددا سعی کنید', + '1066' => 'سیستم سرويس دهی پرداخت موقتا غیر فعال شده است', + '1068' => 'با عرض پوزش به علت بروزرسانی , سیستم موقتا قطع میباشد', + '1072' => 'خطا در پردازش پارامترهای اختیاری پذيرنده', + '1101' => 'مبلغ تراکنش نامعتبر است', + '1103' => 'توکن ارسالی نامعتبر است', + '1104' => 'اطلاعات تسهیم صحیح نیست', + '1105' => 'تراکنش بازگشت داده شده است(مهلت زمانی به پايان رسیده است)' + ]; + + $unknownError = 'خطای ناشناخته رخ داده است. در صورت کسر مبلغ از حساب حداکثر پس از 72 ساعت به حسابتان برمیگردد'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } } diff --git a/src/Drivers/Saman/Saman.php b/src/Drivers/Saman/Saman.php index 8b4c72e..65484e1 100644 --- a/src/Drivers/Saman/Saman.php +++ b/src/Drivers/Saman/Saman.php @@ -37,7 +37,7 @@ class Saman extends Driver public function __construct(Invoice $invoice, $settings) { $this->invoice($invoice); - $this->settings = (object) $settings; + $this->settings = (object)$settings; } /** @@ -53,16 +53,31 @@ public function purchase() $data = array( 'MID' => $this->settings->merchantId, 'ResNum' => $this->invoice->getUuid(), - 'Amount' => $this->invoice->getAmount() * 10, // convert to rial + 'Amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'CellNumber' => '' ); + //set CellNumber for get user cards + if (!empty($this->invoice->getDetails()['mobile'])) { + $data['CellNumber'] = $this->invoice->getDetails()['mobile']; + } + $soap = new \SoapClient( - $this->settings->apiPurchaseUrl + $this->settings->apiPurchaseUrl, + [ + 'encoding' => 'UTF-8', + 'cache_wsdl' => WSDL_CACHE_NONE, + 'stream_context' => stream_context_create([ + 'ssl' => [ + 'ciphers' => 'DEFAULT:!DH', + ], + ]), + ] ); - $response = $soap->RequestToken($data['MID'], $data['ResNum'], $data['Amount']); + $response = $soap->RequestToken($data['MID'], $data['ResNum'], $data['Amount'], $data['CellNumber']); - $status = (int) $response; + $status = (int)$response; if ($status < 0) { // if something has done in a wrong way $this->purchaseFailed($response); @@ -80,7 +95,7 @@ public function purchase() * * @return RedirectionForm */ - public function pay() : RedirectionForm + public function pay(): RedirectionForm { $payUrl = $this->settings->apiPaymentUrl; @@ -102,21 +117,40 @@ public function pay() : RedirectionForm * @throws InvalidPaymentException * @throws \SoapFault */ - public function verify() : ReceiptInterface + public function verify(): ReceiptInterface { $data = array( 'RefNum' => Request::input('RefNum'), 'merchantId' => $this->settings->merchantId, ); - $soap = new \SoapClient($this->settings->apiVerificationUrl); - $status = (int) $soap->VerifyTransaction($data['RefNum'], $data['merchantId']); + $soap = new \SoapClient( + $this->settings->apiVerificationUrl, + [ + 'encoding' => 'UTF-8', + 'cache_wsdl' => WSDL_CACHE_NONE, + 'stream_context' => stream_context_create([ + 'ssl' => [ + 'ciphers' => 'DEFAULT:!DH', + ], + ]), + ] + ); + $status = (int)$soap->VerifyTransaction($data['RefNum'], $data['merchantId']); if ($status < 0) { $this->notVerified($status); } - return $this->createReceipt($data['RefNum']); + $receipt = $this->createReceipt($data['RefNum']); + $receipt->detail([ + 'traceNo' => Request::input('TraceNo'), + 'referenceNo' => Request::input('RRN'), + 'transactionId' => Request::input('RefNum'), + 'cardNo' => Request::input('SecurePan'), + ]); + + return $receipt; } /** @@ -143,22 +177,23 @@ protected function createReceipt($referenceId) protected function purchaseFailed($status) { $translations = array( - -1 => 'خطا در پردازش اطلاعات ارسالی (مشکل در یکی از ورودی ها و ناموفق بودن فراخوانی متد برگشت تراکنش)', - -3 => 'ورودیها حاوی کارکترهای غیرمجاز میباشند.', - -4 => 'کلمه عبور یا کد فروشنده اشتباه است (Merchant Authentication Failed)', - -6 => 'سند قبال برگشت کامل یافته است. یا خارج از زمان 30 دقیقه ارسال شده است.', - -7 => 'رسید دیجیتالی تهی است.', - -8 => 'طول ورودیها بیشتر از حد مجاز است.', - -9 => 'وجود کارکترهای غیرمجاز در مبلغ برگشتی.', - -10 => 'رسید دیجیتالی به صورت Base64 نیست (حاوی کاراکترهای غیرمجاز است)', - -11 => 'طول ورودیها ک تر از حد مجاز است.', - -12 => 'مبلغ برگشتی منفی است.', - -13 => 'مبلغ برگشتی برای برگشت جزئی بیش از مبلغ برگشت نخوردهی رسید دیجیتالی است.', - -14 => 'چنین تراکنشی تعریف نشده است.', - -15 => 'مبلغ برگشتی به صورت اعشاری داده شده است.', - -16 => 'خطای داخلی سیستم', - -17 => 'برگشت زدن جزیی تراکنش مجاز نمی باشد.', - -18 => 'IP Address فروشنده نا معتبر است و یا رمز تابع بازگشتی (reverseTransaction) اشتباه است.', + -1 => ' تراکنش توسط خریدار کنسل شده است.', + -6 => 'سند قابل برگشت کامل یافته است. یا خارج از زمان 30 دقیقه ارسال شده است.', + -18 => 'IP Address فروشنده نا‌معتبر است.', + 79 => 'مبلغ سند برگشتی، از مبلغ تراکنش اصلی بیشتر است.', + 12 => 'درخواست برگشت یک تراکنش رسیده است، در حالی که تراکنش اصلی پیدا نمی شود.', + 14 => 'شماره کارت نامعتبر است.', + 15 => 'چنین صادر کننده کارتی وجود ندارد.', + 33 => 'از تاریخ انقضای کارت گذشته است و کارت دیگر معتبر نیست.', + 38 => 'رمز کارت 3 مرتبه اشتباه وارد شده است در نتیجه کارت غیر فعال خواهد شد.', + 55 => 'خریدار رمز کارت را اشتباه وارد کرده است.', + 61 => 'مبلغ بیش از سقف برداشت می باشد.', + 93 => 'تراکنش Authorize شده است (شماره PIN و PAN درست هستند) ولی امکان سند خوردن وجود ندارد.', + 68 => 'تراکنش در شبکه بانکی Timeout خورده است.', + 34 => 'خریدار یا فیلد CVV2 و یا فیلد ExpDate را اشتباه وارد کرده است (یا اصلا وارد نکرده است).', + 51 => 'موجودی حساب خریدار، کافی نیست.', + 84 => 'سیستم بانک صادر کننده کارت خریدار، در وضعیت عملیاتی نیست.', + 96 => 'کلیه خطاهای دیگر بانکی باعث ایجاد چنین خطایی می گردد.', ); if (array_key_exists($status, $translations)) { @@ -178,28 +213,28 @@ protected function purchaseFailed($status) private function notVerified($status) { $translations = array( - -1 => ' تراکنش توسط خریدار کنسل شده است.', - -6 => 'سند قبال برگشت کامل یافته است. یا خارج از زمان 30 دقیقه ارسال شده است.', - 79 => 'مبلغ سند برگشتی، از مبلغ تراکنش اصلی بیشتر است.', - 12 => 'درخواست برگشت یک تراکنش رسیده است، در حالی که تراکنش اصلی پیدا نمی شود.', - 14 => 'شماره کارت نامعتبر است.', - 15 => 'چنین صادر کننده کارتی وجود ندارد.', - 33 => 'از تاریخ انقضای کارت گذشته است و کارت دیگر معتبر نیست.', - 38 => 'رمز کارت 3 مرتبه اشتباه وارد شده است در نتیجه کارت غیر فعال خواهد شد.', - 55 => 'خریدار رمز کارت را اشتباه وارد کرده است.', - 61 => 'مبلغ بیش از سقف برداشت می باشد.', - 93 => 'تراکنش Authorize شده است (شماره PIN و PAN درست هستند) ولی امکان سند خوردن وجود ندارد.', - 68 => 'تراکنش در شبکه بانکی Timeout خورده است.', - 34 => 'خریدار یا فیلد CVV2 و یا فیلد ExpDate را اشتباه وارد کرده است (یا اصال وارد نکرده است).', - 51 => 'موجودی حساب خریدار، کافی نیست.', - 84 => 'سیستم بانک صادر کننده کارت خریدار، در وضعیت عملیاتی نیست.', - 96 => 'کلیه خطاهای دیگر بانکی باعث ایجاد چنین خطایی می گردد.', + -1 => 'خطا در پردازش اطلاعات ارسالی (مشکل در یکی از ورودی ها و ناموفق بودن فراخوانی متد برگشت تراکنش)', + -3 => 'ورودی ها حاوی کارکترهای غیرمجاز میباشند.', + -4 => 'کلمه عبور یا کد فروشنده اشتباه است (Merchant Authentication Failed)', + -6 => 'سند قابل برگشت کامل یافته است. یا خارج از زمان 30 دقیقه ارسال شده است.', + -7 => 'رسید دیجیتالی تهی است.', + -8 => 'طول ورودی ها بیشتر از حد مجاز است.', + -9 => 'وجود کارکترهای غیرمجاز در مبلغ برگشتی.', + -10 => 'رسید دیجیتالی به صورت Base64 نیست (حاوی کاراکترهای غیرمجاز است)', + -11 => 'طول ورودی ها کمتر از حد مجاز است.', + -12 => 'مبلغ برگشتی منفی است.', + -13 => 'مبلغ برگشتی برای برگشت جزئی بیش از مبلغ برگشت نخورده ی رسید دیجیتالی است.', + -14 => 'چنین تراکنشی تعریف نشده است.', + -15 => 'مبلغ برگشتی به صورت اعشاری داده شده است.', + -16 => 'خطای داخلی سیستم', + -17 => 'برگشت زدن جزیی تراکنش مجاز نمی باشد.', + -18 => 'IP Address فروشنده نا معتبر است و یا رمز تابع بازگشتی (reverseTransaction) اشتباه است.', ); if (array_key_exists($status, $translations)) { - throw new InvalidPaymentException($translations[$status]); + throw new InvalidPaymentException($translations[$status], (int)$status); } else { - throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.'); + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); } } } diff --git a/src/Drivers/Sepehr/Sepehr.php b/src/Drivers/Sepehr/Sepehr.php new file mode 100644 index 0000000..8abb973 --- /dev/null +++ b/src/Drivers/Sepehr/Sepehr.php @@ -0,0 +1,215 @@ +invoice($invoice); + $this->settings = (object)$settings; + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + */ + public function purchase() + { + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + + $mobile = ''; + //set CellNumber for get user cards + if (!empty($this->invoice->getDetails()['mobile'])) { + $mobile = '&CellNumber=' . $this->invoice->getDetails()['mobile']; + } + + $data_query = 'Amount=' . $this->test_input($amount) . '&callbackURL=' . $this->test_input($this->settings->callbackUrl) . '&InvoiceID=' . $this->test_input($this->invoice->getUuid()) . '&TerminalID=' . $this->test_input($this->settings->terminalId) . '&Payload=' . $this->test_input("") . $mobile; + $address_service_token = $this->settings->apiGetToken; + + $token_array = $this->makeHttpChargeRequest('POST', $data_query, $address_service_token); + + if ($token_array === false) { + throw new PurchaseFailedException('درگاه مورد نظر پاسخگو نمی‌باشد، لطفا لحظاتی بعد امتحان کنید.'); + } + + $decode_token_array = json_decode($token_array); + + $status = $decode_token_array->Status; + $access_token = $decode_token_array->Accesstoken; + + if (empty($access_token) && $status != 0) { + $this->purchaseFailed($status); + } + + $this->invoice->transactionId($access_token); + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + return $this->redirectWithForm($this->settings->apiPaymentUrl, [ + 'token' => $this->invoice->getTransactionId(), + 'terminalID' => $this->settings->terminalId + ], 'POST'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * + */ + public function verify(): ReceiptInterface + { + $responseCode = Request::input('respcode'); + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + + if ($responseCode != 0) { + $this->notVerified($responseCode); + } + + $data_query = 'digitalreceipt=' . Request::input('digitalreceipt') . '&Tid=' . $this->settings->terminalId; + $advice_array = $this->makeHttpChargeRequest('POST', $data_query, $this->settings->apiVerificationUrl); + $decode_advice_array = json_decode($advice_array); + + $status = $decode_advice_array->Status; + $return_id = $decode_advice_array->ReturnId; + + if ($status == "Ok") { + if ($return_id != $amount) { + throw new InvalidPaymentException('مبلغ واریز با قیمت محصول برابر نیست'); + } + return $this->createReceipt(Request::input('rrn')); + } else { + $message = 'تراکنش نا موفق بود در صورت کسر مبلغ از حساب شما حداکثر پس از 72 ساعت مبلغ به حسابتان برمیگردد.'; + throw new InvalidPaymentException($message); + } + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId) + { + $receipt = new Receipt('sepehr', $referenceId); + + return $receipt; + } + + /** + * Trigger an exception + * + * @param $status + * + * @throws PurchaseFailedException + */ + protected function purchaseFailed($status) + { + $translations = array( + -1 => 'تراکنش پیدا نشد.', + -2 => 'عدم تطابق ip و یا بسته بودن port 8081', + -3 => '‫ها‬ ‫‪Exception‬‬ ‫خطای‬ ‫–‬ ‫عمومی‬ ‫خطای‬ ‫‪Total‬‬ ‫‪Error‬‬', + -4 => 'امکان انجام درخواست برای این تراکنش وجود ندارد.', + -5 => 'آدرس ip نامعتبر می‌باشد.', + -6 => 'عدم فعال بودن سرویس برگشت تراکنش برای پذیرنده', + ); + + if (array_key_exists($status, $translations)) { + throw new PurchaseFailedException($translations[$status]); + } else { + throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); + } + } + + /** + * Trigger an exception + * + * @param $status + * + * @throws InvalidPaymentException + */ + private function notVerified($status) + { + $translations = array( + -1 => ' تراکنش توسط خریدار کنسل شده است.', + -2 => 'زمان انجام تراکنش برای کاربر به پایان رسیده است.', + -3 => '‫ها‬ ‫‪Exception‬‬ ‫خطای‬ ‫–‬ ‫عمومی‬ ‫خطای‬ ‫‪Total‬‬ ‫‪Error‬‬', + -4 => 'امکان انجام درخواست برای این تراکنش وجود ندارد.', + -5 => 'آدرس ip نامعتبر می‌باشد.', + -6 => 'عدم فعال بودن سرویس برگشت تراکنش برای پذیرنده', + ); + + if (array_key_exists($status, $translations)) { + throw new InvalidPaymentException($translations[$status], (int)$status); + } else { + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); + } + } + + private function test_input($data) + { + $data = trim($data); + $data = stripslashes($data); + $data = htmlspecialchars($data); + return $data; + } + + private function makeHttpChargeRequest($_Method, $_Data, $_Address) + { + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $_Address); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $_Method); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $_Data); + $result = curl_exec($curl); + curl_close($curl); + return $result; + } +} diff --git a/src/Drivers/Sepordeh/Sepordeh.php b/src/Drivers/Sepordeh/Sepordeh.php new file mode 100644 index 0000000..8d66100 --- /dev/null +++ b/src/Drivers/Sepordeh/Sepordeh.php @@ -0,0 +1,223 @@ +invoice($invoice); + $this->settings = (object)$settings; + $this->client = new Client(); + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function purchase() + { + $orderId = $this->extractDetails('orderId'); + $phone = $this->extractDetails('phone'); + $description = $this->extractDetails('description') ?: $this->settings->description; + + $data = [ + "merchant" => $this->settings->merchantId, + "amount" => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman + "phone" => $phone, + "orderId" => $orderId, + "callback" => $this->settings->callbackUrl, + "description" => $description, + ]; + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPurchaseUrl, + [ + "form_params" => $data, + "http_errors" => false, + 'verify' => false, + ] + ); + + $responseBody = mb_strtolower($response->getBody()->getContents()); + $body = @json_decode($responseBody, true); + $statusCode = (int)$body['status']; + + if ($statusCode !== 200) { + // some error has happened + $message = $body['message'] ?? $this->convertStatusCodeToMessage($statusCode); + + throw new PurchaseFailedException($message); + } + + $this->invoice->transactionId($body['information']['invoice_id']); + + return $this->invoice->getTransactionId(); + } + + /** + * Retrieve data from details using its name. + * + * @return string + */ + private function extractDetails($name) + { + return empty($this->invoice->getDetails()[$name]) ? null : $this->invoice->getDetails()[$name]; + } + + /** + * Retrieve related message to given status code + * + * @param $statusCode + * + * @return string + */ + private function convertStatusCodeToMessage(int $statusCode): string + { + $messages = [ + 400 => 'مشکلی در ارسال درخواست وجود دارد', + 401 => 'عدم دسترسی', + 403 => 'دسترسی غیر مجاز', + 404 => 'آیتم درخواستی مورد نظر موجود نمی باشد', + 500 => 'مشکلی در سرور درگاه پرداخت رخ داده است', + 503 => 'سرور درگاه پرداخت در حال حاضر قادر به پاسخگویی نمی باشد', + ]; + + $unknown = 'خطای ناشناخته ای در درگاه پرداخت رخ داده است'; + + return $messages[$statusCode] ?? $unknown; + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + $basePayUrl = $this->settings->mode == 'normal' ? $this->settings->apiPaymentUrl + : $this->settings->apiDirectPaymentUrl; + $payUrl = $basePayUrl . $this->invoice->getTransactionId(); + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verify(): ReceiptInterface + { + $authority = $this->invoice->getTransactionId() ?? Request::input('authority'); + $data = [ + 'merchant' => $this->settings->merchantId, + 'authority' => $authority, + ]; + + $response = $this->client->request( + 'POST', + $this->settings->apiVerificationUrl, + [ + 'form_params' => $data, + "headers" => [ + "http_errors" => false, + ], + 'verify' => false, + ] + ); + + $responseBody = mb_strtolower($response->getBody()->getContents()); + $body = @json_decode($responseBody, true); + $statusCode = (int)$body['status']; + + if ($statusCode !== 200) { + $message = $body['message'] ?? $this->convertStatusCodeToMessage($statusCode); + + $this->notVerified($message, $statusCode); + } + + $refId = $body['information']['invoice_id']; + $detail = [ + 'card' => $body['information']['card'], + 'orderId' => Request::input('orderId') + ]; + + return $this->createReceipt($refId, $detail); + } + + /** + * Trigger an exception + * + * @param $message + * + * @throws InvalidPaymentException + */ + private function notVerified($message, $status) + { + throw new InvalidPaymentException($message, (int)$status); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId, $detail = []) + { + $receipt = new Receipt('sepordeh', $referenceId); + $receipt->detail($detail); + + return $receipt; + } +} diff --git a/src/Drivers/Sizpay/Sizpay.php b/src/Drivers/Sizpay/Sizpay.php new file mode 100644 index 0000000..1a08223 --- /dev/null +++ b/src/Drivers/Sizpay/Sizpay.php @@ -0,0 +1,162 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + $client = new \SoapClient($this->settings->apiPurchaseUrl); + $response = $client->GetToken2(array( + 'MerchantID' => $this->settings->merchantId, + 'TerminalID' => $this->settings->terminal, + 'UserName' => $this->settings->username, + 'Password' => $this->settings->password, + 'Amount' => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + 'OrderID' => time(), + 'ReturnURL' => $this->settings->callbackUrl, + 'InvoiceNo' => time(), + 'DocDate' => '', + 'ExtraInf' => time(), + 'AppExtraInf' => '', + 'SignData' => $this->settings->SignData + ))->GetToken2Result; + $result = json_decode($response); + + if (! isset($result->ResCod) || ! in_array($result->ResCod, ['0', '00'])) { + // error has happened + $message = $result->Message ?? 'خطای ناشناخته رخ داده'; + throw new PurchaseFailedException($message); + } + + $this->invoice->transactionId($result->Token); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay() : RedirectionForm + { + $payUrl = $this->settings->apiPaymentUrl; + + return $this->redirectWithForm( + $payUrl, + [ + 'MerchantID' => $this->settings->merchantId, + 'TerminalID' => $this->settings->terminal, + 'Token' => $this->invoice->getTransactionId(), + 'SignData' => $this->settings->SignData + ], + 'POST' + ); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function verify() : ReceiptInterface + { + $resCode = Request::input('ResCod'); + if (! in_array($resCode, array('0', '00'))) { + $message = 'پرداخت توسط کاربر لغو شد'; + throw new InvalidPaymentException($message); + } + + $data = array( + 'MerchantID' => $this->settings->merchantId, + 'TerminalID' => $this->settings->terminal, + 'UserName' => $this->settings->username, + 'Password' => $this->settings->password, + 'Token' => Request::input('Token'), + 'SignData' => $this->settings->SignData + ); + + $client = new \SoapClient($this->settings->apiVerificationUrl); + $response = $client->Confirm2($data)->Confirm2Result; + $result = json_decode($response); + + if (! isset($result->ResCod) || ! in_array($result->ResCod, array('0', '00'))) { + $message = $result->Message ?? 'خطا در انجام عملیات رخ داده است'; + throw new InvalidPaymentException($message, (int)(isset($result->ResCod) ? $result->ResCod : 0)); + } + + return $this->createReceipt($result->RefNo); + } + + /** + * Generate the payment's receipt + * + * @param $resCode + * + * @return Receipt + */ + protected function createReceipt($resCode) + { + $receipt = new Receipt('sizpay', $resCode); + + return $receipt; + } +} diff --git a/src/Drivers/SnappPay/SnappPay.php b/src/Drivers/SnappPay/SnappPay.php new file mode 100644 index 0000000..183cf9a --- /dev/null +++ b/src/Drivers/SnappPay/SnappPay.php @@ -0,0 +1,455 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + $this->oauthToken = $this->oauth(); + } + + /** + * @throws PurchaseFailedException + */ + public function purchase(): string + { + $phone = $this->invoice->getDetail('phone') + ?? $this->invoice->getDetail('cellphone') + ?? $this->invoice->getDetail('mobile'); + + // convert to format +98 901 XXX XXXX + $phone = preg_replace('/^0/', '+98', $phone); + + $data = [ + 'amount' => $this->normalizerAmount($this->invoice->getAmount()), + 'mobile' => $phone, + 'paymentMethodTypeDto' => 'INSTALLMENT', + 'transactionId' => $this->invoice->getUuid(), + 'returnURL' => $this->settings->callbackUrl, + ]; + + if (!is_null($discountAmount = $this->invoice->getDetail('discountAmount'))) { + $data['discountAmount'] = $this->normalizerAmount($discountAmount); + } + + if (!is_null($externalSourceAmount = $this->invoice->getDetail('externalSourceAmount'))) { + $data['externalSourceAmount'] = $this->normalizerAmount($externalSourceAmount) ; + } + + if (is_null($this->invoice->getDetail('cartList'))) { + throw new PurchaseFailedException('"cartList" is required for this driver'); + } + + $data['cartList'] = $this->invoice->getDetail('cartList'); + + $this->normalizerCartList($data); + + $response = $this + ->client + ->post( + $this->settings->apiPaymentUrl.self::TOKEN_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || $body['successful'] === false) { + // error has happened + $message = $body['errorData']['message'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.'; + throw new PurchaseFailedException($message); + } + + $this->invoice->transactionId($body['response']['paymentToken']); + $this->setPaymentUrl($body['response']['paymentPageUrl']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + public function pay(): RedirectionForm + { + parse_str(parse_url($this->getPaymentUrl(), PHP_URL_QUERY), $formData); + + return $this->redirectWithForm($this->getPaymentUrl(), $formData, 'GET'); + } + + /** + * @throws InvalidPaymentException + */ + public function verify(): ReceiptInterface + { + $data = [ + 'paymentToken' => $this->invoice->getTransactionId(), + ]; + + $response = $this + ->client + ->post( + $this->settings->apiPaymentUrl.self::VERIFY_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || $body['successful'] === false) { + // error has happened + $message = $body['errorData']['message'] ?? 'خطا در هنگام تایید تراکنش'; + throw new PurchaseFailedException($message); + } + + return (new Receipt('snapppay', $body['response']['transactionId']))->detail($body['response']); + } + + /** + * @throws PurchaseFailedException + */ + protected function oauth() + { + $response = $this + ->client + ->post( + $this->settings->apiPaymentUrl.self::OAUTH_URL, + [ + RequestOptions::HEADERS => [ + 'Authorization' => 'Basic '.base64_encode("{$this->settings->client_id}:{$this->settings->client_secret}"), + ], + RequestOptions::FORM_PARAMS => [ + 'grant_type' => 'password', + 'scope' => 'online-merchant', + 'username' => $this->settings->username, + 'password' => $this->settings->password, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + if ($response->getStatusCode() != 200) { + throw new PurchaseFailedException('خطا در هنگام احراز هویت.'); + } + + $body = json_decode($response->getBody()->getContents(), true); + + return $body['access_token']; + } + + /** + * @throws PurchaseFailedException + */ + public function eligible() + { + if (is_null($amount = $this->invoice->getAmount())) { + throw new PurchaseFailedException('"amount" is required for this method.'); + } + + $response = $this->client->get($this->settings->apiPaymentUrl.self::ELIGIBLE_URL, [ + RequestOptions::HEADERS => [ + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::QUERY => [ + 'amount' => $this->normalizerAmount($amount), + ], + ]); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200) { + $message = $body['errorData']['message'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.'; + throw new InvalidPaymentException($message, (int) $response->getStatusCode()); + } + + return $body; + } + + private function normalizerAmount(int $amount): int + { + return $amount * ($this->settings->currency == 'T' ? 10 : 1); + } + + private function normalizerCartList(array &$data): void + { + if (isset($data['cartList']['shippingAmount'])) { + $data['cartList'] = [$data['cartList']]; + } + + foreach ($data['cartList'] as &$item) { + if (isset($item['shippingAmount'])) { + $item['shippingAmount'] = $this->normalizerAmount($item['shippingAmount']); + } + + if (isset($item['taxAmount'])) { + $item['taxAmount'] = $this->normalizerAmount($item['taxAmount']); + } + + if (isset($item['totalAmount'])) { + $item['totalAmount'] = $this->normalizerAmount($item['totalAmount']); + } + + foreach ($item['cartItems'] as &$cartItem) { + $cartItem['amount'] = $this->normalizerAmount($cartItem['amount']); + } + } + } + + public function settle(): array + { + $data = [ + 'paymentToken' => $this->invoice->getTransactionId(), + ]; + + $response = $this + ->client + ->post( + $this->settings->apiPaymentUrl.self::SETTLE_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || $body['successful'] === false) { + // error has happened + $message = $body['errorData']['message'] ?? 'خطا در Settle تراکنش'; + throw new PurchaseFailedException($message); + } + + return $body['response']; + } + + public function revert() + { + $data = [ + 'paymentToken' => $this->invoice->getTransactionId(), + ]; + + $response = $this + ->client + ->post( + $this->settings->apiPaymentUrl.self::REVERT_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || $body['successful'] === false) { + // error has happened + $message = $body['errorData']['message'] ?? 'خطا در Revert تراکنش'; + throw new PurchaseFailedException($message); + } + + return $body['response']; + } + + public function status() + { + $data = [ + 'paymentToken' => $this->invoice->getTransactionId(), + ]; + + $response = $this + ->client + ->get( + $this->settings->apiPaymentUrl.self::STATUS_URL, + [ + RequestOptions::QUERY => $data, + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || $body['successful'] === false) { + // error has happened + $message = $body['errorData']['message'] ?? 'خطا در status تراکنش'; + throw new PurchaseFailedException($message); + } + + return $body['response']; + } + + public function cancel() + { + $data = [ + 'paymentToken' => $this->invoice->getTransactionId(), + ]; + + $response = $this + ->client + ->post( + $this->settings->apiPaymentUrl.self::CANCEL_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || $body['successful'] === false) { + // error has happened + $message = $body['errorData']['message'] ?? 'خطا در Cancel تراکنش'; + throw new PurchaseFailedException($message); + } + + return $body['response']; + } + + public function update() + { + $data = [ + 'amount' => $this->normalizerAmount($this->invoice->getAmount()), + 'paymentMethodTypeDto' => 'INSTALLMENT', + 'paymentToken' => $this->invoice->getTransactionId(), + ]; + + if (!is_null($discountAmount = $this->invoice->getDetail('discountAmount'))) { + $data['discountAmount'] = $this->normalizerAmount($discountAmount); + } + + if (!is_null($externalSourceAmount = $this->invoice->getDetail('externalSourceAmount'))) { + $data['externalSourceAmount'] = $this->normalizerAmount($externalSourceAmount) ; + } + + if (is_null($this->invoice->getDetail('cartList'))) { + throw new PurchaseFailedException('"cartList" is required for this driver'); + } + + $data['cartList'] = $this->invoice->getDetail('cartList'); + + $this->normalizerCartList($data); + + $response = $this + ->client + ->post( + $this->settings->apiPaymentUrl.self::UPDATE_URL, + [ + RequestOptions::BODY => json_encode($data), + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken, + ], + RequestOptions::HTTP_ERRORS => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200 || $body['successful'] === false) { + // error has happened + $message = $body['errorData']['message'] ?? 'خطا در بروزرسانی تراکنش رخ داده است.'; + throw new PurchaseFailedException($message); + } + + return $body['response']; + } + + private function getPaymentUrl(): string + { + return $this->paymentUrl; + } + + private function setPaymentUrl(string $paymentUrl): void + { + $this->paymentUrl = $paymentUrl; + } +} diff --git a/src/Drivers/Toman/Toman.php b/src/Drivers/Toman/Toman.php new file mode 100644 index 0000000..cae76eb --- /dev/null +++ b/src/Drivers/Toman/Toman.php @@ -0,0 +1,102 @@ +invoice($invoice); // Set the invoice. + $this->settings = (object) $settings; // Set settings. + $this->base_url = $this->settings->base_url; + $this->shop_slug = $this->settings->shop_slug; + $this->auth_code = $this->settings->auth_code; + $this->code = $this->shop_slug . ':' . $this->auth_code; + $this->auth_token = base64_encode($this->code); + } + + // Purchase the invoice, save its transactionId and finaly return it. + public function purchase() + { + $url = $this->base_url . "/users/me/shops/" . $this->shop_slug . "/deals"; + $data = $this->settings->data; + + $response = Http::withHeaders([ + 'Authorization' => "Basic {$this->auth_token}", + "Content-Type" => 'application/json' + ])->post($url, $data); + + $result = json_decode($response->getBody()->getContents(), true); + + if (isset($result['trace_number'])) { + $this->invoice->transactionId($result['trace_number']); + return $this->invoice->getTransactionId(); + } else { + throw new InvalidPaymentException('پرداخت با مشکل مواجه شد، لطفا با ما در ارتباط باشید'); + } + } + + // Redirect into bank using transactionId, to complete the payment. + public function pay(): RedirectionForm + { + $transactionId = $this->invoice->getTransactionId(); + $redirect_url = $this->base_url . '/deals/' . $transactionId . '/redirect'; + + return $this->redirectWithForm($redirect_url, [], 'GET'); + } + + // Verify the payment (we must verify to ensure that user has paid the invoice). + public function verify(): ReceiptInterface + { + $state = Request::input('state'); + + $transactionId = $this->invoice->getTransactionId(); + $verifyUrl = $this->base_url . "/users/me/shops/" . $this->shop_slug . "/deals/" . $transactionId . "/verify"; + + if ($state != 'funded') { + throw new InvalidPaymentException('پرداخت انجام نشد'); + } + + Http::withHeaders([ + 'Authorization' => "Basic {$this->auth_token}", + "Content-Type" => 'application/json' + ])->patch($verifyUrl); + + return $this->createReceipt($transactionId); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + public function createReceipt($referenceId) + { + return new Receipt('toman', $referenceId); + } +} diff --git a/src/Drivers/Vandar/Vandar.php b/src/Drivers/Vandar/Vandar.php new file mode 100644 index 0000000..a06643a --- /dev/null +++ b/src/Drivers/Vandar/Vandar.php @@ -0,0 +1,204 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + } + + /** + * Retrieve data from details using its name. + * + * @return string + */ + private function extractDetails($name) + { + return empty($this->invoice->getDetails()[$name]) ? null : $this->invoice->getDetails()[$name]; + } + + /** + * @return string + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Shetabit\Multipay\Exceptions\PurchaseFailedException + */ + public function purchase() + { + $mobile = $this->extractDetails('mobile'); + $description = $this->extractDetails('description'); + $nationalCode = $this->extractDetails('national_code'); + $validCard = $this->extractDetails('valid_card_number'); + $factorNumber = $this->extractDetails('factorNumber'); + + $data = [ + 'api_key' => $this->settings->merchantId, + 'amount' => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman + 'callback_url' => $this->settings->callbackUrl, + 'description' => $description, + 'mobile_number' => $mobile, + 'national_code' => $nationalCode, + 'valid_card_number' => $validCard, + 'factorNumber' => $factorNumber, + ]; + + $response = $this->client + ->request( + 'POST', + $this->settings->apiPurchaseUrl, + [ + 'json' => $data, + 'headers' => [ + "Accept" => "application/json", + ], + 'http_errors' => false, + ] + ); + + $responseBody = json_decode($response->getBody()->getContents(), true); + $statusCode = (int) $responseBody['status']; + + if ($statusCode !== 1) { + $errors = array_pop($responseBody['errors']); + + throw new PurchaseFailedException($errors); + } + + $this->invoice->transactionId($responseBody['token']); + + return $this->invoice->getTransactionId(); + } + + /** + * @return \Shetabit\Multipay\RedirectionForm + */ + public function pay(): RedirectionForm + { + $url = $this->settings->apiPaymentUrl . $this->invoice->getTransactionId(); + + return $this->redirectWithForm($url, [], 'GET'); + } + + /** + * @return ReceiptInterface + * + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Shetabit\Multipay\Exceptions\InvalidPaymentException + */ + public function verify(): ReceiptInterface + { + $token = Request::get('token'); + $paymentStatus = Request::get('payment_status'); + $data = [ + 'api_key' => $this->settings->merchantId, + 'token' => $token + ]; + + if ($paymentStatus == self::PAYMENT_STATUS_FAILED) { + $this->notVerified('پرداخت با شکست مواجه شد.'); + } + + $response = $this->client + ->request( + 'POST', + $this->settings->apiVerificationUrl, + [ + 'json' => $data, + 'headers' => [ + "Accept" => "application/json", + ], + 'http_errors' => false, + ] + ); + + $responseBody = json_decode($response->getBody()->getContents(), true); + $statusCode = (int) $responseBody['status']; + + if ($statusCode !== 1) { + if (isset($responseBody['error'])) { + $message = is_array($responseBody['error']) ? array_pop($responseBody['error']) : $responseBody['error']; + } + + if (isset($responseBody['errors']) and is_array($responseBody['errors'])) { + $message = array_pop($responseBody['errors']); + } + + $this->notVerified($message ?? '', $statusCode); + } + + $receipt = $this->createReceipt($token); + + $receipt->detail([ + "amount" => $responseBody['amount'], + "realAmount" => $responseBody['realAmount'], + "wage" => $responseBody['wage'], + "cardNumber" => $responseBody['cardNumber'], + ]); + + return $receipt; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId) + { + $receipt = new Receipt('vandar', $referenceId); + + return $receipt; + } + + /** + * @param $message + * @throws \Shetabit\Multipay\Exceptions\InvalidPaymentException + */ + protected function notVerified($message, $status = 0) + { + if (empty($message)) { + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', (int)$status); + } else { + throw new InvalidPaymentException($message, (int)$status); + } + } +} diff --git a/src/Drivers/Walleta/Walleta.php b/src/Drivers/Walleta/Walleta.php new file mode 100644 index 0000000..fd3dedd --- /dev/null +++ b/src/Drivers/Walleta/Walleta.php @@ -0,0 +1,226 @@ +invoice($invoice); + $this->settings = (object)$settings; + } + + /** + * Purchase Invoice.09214125578 + * + * @return string + * + * @throws PurchaseFailedException + */ + public function purchase() + { + $result = $this->token(); + + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['content']['type']); + } + + $this->invoice->transactionId($result['content']['token']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + return $this->redirectWithForm($this->settings->apiPaymentUrl . $this->invoice->getTransactionId(), [], 'GET'); + } + + /** + * Verify payment + * + * @return mixed|Receipt + * + * @throws PurchaseFailedException + */ + public function verify(): ReceiptInterface + { + $result = $this->verifyTransaction(); + + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['content']['type']); + } + + $receipt = $this->createReceipt($this->invoice->getTransactionId()); + + return $receipt; + } + + /** + * send request to Walleta + * + * @param $method + * @param $url + * @param array $data + * @return array + */ + protected function callApi($method, $url, $data = []): array + { + $client = new Client(); + + $response = $client->request($method, $url, [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ]); + + return [ + 'status_code' => $response->getStatusCode(), + 'content' => json_decode($response->getBody()->getContents(), true) + ]; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId): Receipt + { + $receipt = new Receipt('walleta', $referenceId); + + return $receipt; + } + + /** + * call create token request + * + * @return array + */ + public function token(): array + { + return $this->callApi('POST', $this->settings->apiPurchaseUrl, [ + 'merchant_code' => $this->settings->merchantId, + 'invoice_reference' => $this->invoice->getUuid(), + 'invoice_date' => date('Y-m-d H:i:s'), + 'invoice_amount' => $this->invoice->getAmount(), + 'payer_first_name' => $this->invoice->getDetails()['first_name'], + 'payer_last_name' => $this->invoice->getDetails()['last_name'], + 'payer_national_code' => $this->invoice->getDetails()['national_code'], + 'payer_mobile' => $this->invoice->getDetails()['mobile'], + 'callback_url' => $this->settings->callbackUrl, + 'items' => $this->getItems(), + ]); + } + + /** + * call verift transaction request + * + * @return array + */ + public function verifyTransaction(): array + { + return $this->callApi('POST', $this->settings->apiVerificationUrl, [ + 'merchant_code' => $this->settings->merchantId, + 'token' => $this->invoice->getTransactionId(), + 'invoice_reference' => $this->invoice->getDetail('uuid'), + 'invoice_amount' => $this->invoice->getAmount(), + ]); + } + + /** + * get Items for + * + * + */ + private function getItems() + { + /** + * example data + * + * $items = [ + * [ + * "reference" => "string", + * "name" => "string", + * "quantity" => 0, + * "unit_price" => 0, + * "unit_discount" => 0, + * "unit_tax_amount" => 0, + * "total_amount" => 0 + * ] + * ]; + */ + return $this->invoice->getDetails()['items']; + } + + + /** + * Trigger an exception + * + * @param $status + * + * @throws PurchaseFailedException + */ + protected function purchaseFailed($status) + { + $translations = [ + "server_error" => "یک خطای داخلی رخ داده است.", + "ip_address_error" => "آدرس IP پذیرنده صحیح نیست.", + "validation_error" => "اطلاعات ارسال شده صحیح نیست.", + "merchant_error" => "کد پذیرنده معتبر نیست.", + "payment_token_error" => "شناسه پرداخت معتبر نیست.", + "invoice_amount_error" => "مبلغ تراکنش با مبلغ پرداخت شده مطابقت ندارد.", + ]; + + if (array_key_exists($status, $translations)) { + throw new PurchaseFailedException($translations[$status]); + } else { + throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); + } + } +} diff --git a/src/Drivers/Yekpay/Yekpay.php b/src/Drivers/Yekpay/Yekpay.php index 060d7c5..387d11a 100644 --- a/src/Drivers/Yekpay/Yekpay.php +++ b/src/Drivers/Yekpay/Yekpay.php @@ -10,6 +10,8 @@ use Shetabit\Multipay\Receipt; use Shetabit\Multipay\RedirectionForm; use Shetabit\Multipay\Request; +use SoapClient; +use stdClass; class Yekpay extends Driver { @@ -60,9 +62,9 @@ private function extractDetails($name) */ public function purchase() { - $client = new \SoapClient($this->settings->apiPurchaseUrl, array('trace' => true)); + $client = new SoapClient($this->settings->apiPurchaseUrl, array('trace' => true)); - $data = new \stdClass(); + $data = new stdClass(); if (!empty($this->invoice->getDetails()['description'])) { $description = $this->invoice->getDetails()['description']; @@ -128,7 +130,7 @@ public function verify() : ReceiptInterface $options = array('trace' => true); $client = new SoapClient($this->settings->apiVerificationUrl, $options); - $data = new \stdClass(); + $data = new stdClass(); $data->merchantId = $this->settings->merchantId; $data->authority = $this->invoice->getTransactionId() ?? Request::input('authority'); @@ -136,7 +138,7 @@ public function verify() : ReceiptInterface $response = json_decode($client->verify($data)); if ($response->Code != 100) { - $this->notVerified($response->message ?? 'payment failed'); + $this->notVerified($response->message ?? 'payment failed', $response->Code); } //"Success Payment with reference: $response->Reference and message: $response->message"; @@ -148,9 +150,9 @@ public function verify() : ReceiptInterface * * @param $referenceId * - * @return Receipt + * @return ReceiptInterface */ - protected function createReceipt($referenceId) + protected function createReceipt($referenceId) : ReceiptInterface { $receipt = new Receipt('yekpay', $referenceId); @@ -161,14 +163,16 @@ protected function createReceipt($referenceId) * Trigger an exception * * @param $message + * @param $status + * * @throws InvalidPaymentException */ - private function notVerified($message) + private function notVerified($message, $status) { if ($message) { - throw new InvalidPaymentException($message); + throw new InvalidPaymentException($message, (int)$status); } else { - throw new InvalidPaymentException('payment failed'); + throw new InvalidPaymentException('payment failed', (int)$status); } } } diff --git a/src/Drivers/Zarinpal/Strategies/Normal.php b/src/Drivers/Zarinpal/Strategies/Normal.php new file mode 100644 index 0000000..5784bb1 --- /dev/null +++ b/src/Drivers/Zarinpal/Strategies/Normal.php @@ -0,0 +1,266 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial + + if (!empty($this->invoice->getDetails()['description'])) { + $description = $this->invoice->getDetails()['description']; + } else { + $description = $this->settings->description; + } + + $data = [ + "merchant_id" => $this->settings->merchantId, + "amount" => $amount, + "currency" => 'IRR', + "callback_url" => $this->settings->callbackUrl, + "description" => $description, + "metadata" => $this->metadata(), + ]; + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPurchaseUrl, + [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ] + ); + + $result = json_decode($response->getBody()->getContents(), true); + + if (!empty($result['errors']) || empty($result['data']) || $result['data']['code'] != 100) { + $bodyResponse = $result['errors']['code']; + throw new PurchaseFailedException($this->translateStatus($bodyResponse), $bodyResponse); + } + + $this->invoice->transactionId($result['data']["authority"]); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + $transactionId = $this->invoice->getTransactionId(); + $paymentUrl = $this->getPaymentUrl(); + + $payUrl = $paymentUrl . $transactionId; + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + */ + public function verify(): ReceiptInterface + { + $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); + $data = [ + "merchant_id" => $this->settings->merchantId, + "authority" => $authority, + "amount" => $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1), // convert to rial + ]; + + $response = $this->client->request( + 'POST', + $this->getVerificationUrl(), + [ + 'json' => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ] + ); + + $result = json_decode($response->getBody()->getContents(), true); + + if (empty($result['data']) || !isset($result['data']['ref_id']) || ($result['data']['code'] != 100 && $result['data']['code'] != 101)) { + $bodyResponse = $result['errors']['code']; + throw new InvalidPaymentException($this->translateStatus($bodyResponse), $bodyResponse); + } + + $refId = $result['data']['ref_id']; + + $receipt = $this->createReceipt($refId); + $receipt->detail([ + 'code' => $result['data']['code'], + 'message' => $result['data']['message'] ?? null, + 'card_hash' => $result['data']['card_hash'] ?? null, + 'card_pan' => $result['data']['card_pan'] ?? null, + 'ref_id' => $refId, + 'fee_type' => $result['data']['fee_type'] ?? null, + 'fee' => $result['data']['fee'] ?? null, + 'order_id' => $result['data']['order_id'] ?? null, + ]); + + return $receipt; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + public function createReceipt($referenceId) + { + return new Receipt('zarinpal', $referenceId); + } + + /** + * Retrieve purchase url + * + * @return string + */ + protected function getPurchaseUrl(): string + { + return $this->settings->apiPurchaseUrl; + } + + /** + * Retrieve Payment url + * + * @return string + */ + protected function getPaymentUrl(): string + { + return $this->settings->apiPaymentUrl; + } + + /** + * Retrieve verification url + * + * @return string + */ + protected function getVerificationUrl(): string + { + return $this->settings->apiVerificationUrl; + } + + /** + * Convert status to a readable message. + * + * @param $status + * + * @return mixed|string + */ + private function translateStatus($status) + { + $translations = [ + '100' => 'تراکنش با موفقیت انجام گردید', + '101' => 'عمليات پرداخت موفق بوده و قبلا عملیات وریفای تراكنش انجام شده است', + '-9' => 'خطای اعتبار سنجی', + '-10' => 'ای پی و يا مرچنت كد پذيرنده صحيح نمی باشد', + '-11' => 'مرچنت کد فعال نیست لطفا با تیم پشتیبانی ما تماس بگیرید', + '-12' => 'تلاش بیش از حد در یک بازه زمانی کوتاه', + '-15' => 'ترمینال شما به حالت تعلیق در آمده با تیم پشتیبانی تماس بگیرید', + '-16' => 'سطح تاييد پذيرنده پايين تر از سطح نقره ای می باشد', + '-30' => 'اجازه دسترسی به تسویه اشتراکی شناور ندارید', + '-31' => 'حساب بانکی تسویه را به پنل اضافه کنید مقادیر وارد شده برای تسهیم صحيح نمی باشد', + '-32' => 'مقادیر وارد شده برای تسهیم صحيح نمی باشد', + '-33' => 'درصد های وارد شده صحيح نمی باشد', + '-34' => 'مبلغ از کل تراکنش بیشتر است', + '-35' => 'تعداد افراد دریافت کننده تسهیم بیش از حد مجاز است', + '-40' => 'پارامترهای اضافی نامعتبر، expire_in معتبر نیست', + '-50' => 'مبلغ پرداخت شده با مقدار مبلغ در وریفای متفاوت است', + '-51' => 'پرداخت ناموفق', + '-52' => 'خطای غیر منتظره با پشتیبانی تماس بگیرید', + '-53' => 'اتوریتی برای این مرچنت کد نیست', + '-54' => 'اتوریتی نامعتبر است', + ]; + + $unknownError = 'خطای ناشناخته رخ داده است. در صورت کسر مبلغ از حساب حداکثر پس از 72 ساعت به حسابتان برمیگردد'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } + + private function metadata(): array + { + $metadata = []; + + if (!empty($this->invoice->getDetails()['mobile'])) { + $metadata['mobile'] = $this->invoice->getDetails()['mobile']; + } + + if (!empty($this->invoice->getDetails()['email'])) { + $metadata['email'] = $this->invoice->getDetails()['email']; + } + + return array_merge($this->invoice->getDetails() ?? [], $metadata); + } +} diff --git a/src/Drivers/Zarinpal/Strategies/Sandbox.php b/src/Drivers/Zarinpal/Strategies/Sandbox.php new file mode 100644 index 0000000..9175410 --- /dev/null +++ b/src/Drivers/Zarinpal/Strategies/Sandbox.php @@ -0,0 +1,209 @@ +invoice($invoice); + $this->settings = (object) $settings; + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + if (!empty($this->invoice->getDetails()['description'])) { + $description = $this->invoice->getDetails()['description']; + } else { + $description = $this->settings->description; + } + + $mobile = !empty($this->invoice->getDetails()['mobile']) ? $this->invoice->getDetails()['mobile'] : ''; + $email = !empty($this->invoice->getDetails()['email']) ? $this->invoice->getDetails()['email'] : ''; + $amount = $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10); // convert to toman + + $data = array( + 'MerchantID' => $this->settings->merchantId, + 'Amount' => $amount, + 'CallbackURL' => $this->settings->callbackUrl, + 'Description' => $description, + 'Mobile' => $mobile ?? '', + 'Email' => $email ?? '', + 'AdditionalData' => $this->invoice->getDetails() + ); + + $client = new SoapClient($this->getPurchaseUrl(), ['encoding' => 'UTF-8']); + $result = $client->PaymentRequest($data); + + $bodyResponse = $result->Status; + if ($bodyResponse != 100 || empty($result->Authority)) { + throw new PurchaseFailedException($this->translateStatus($bodyResponse), $bodyResponse); + } + + $this->invoice->transactionId($result->Authority); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay() : RedirectionForm + { + $transactionId = $this->invoice->getTransactionId(); + $paymentUrl = $this->getPaymentUrl(); + + $payUrl = $paymentUrl.$transactionId; + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \SoapFault + */ + public function verify() : ReceiptInterface + { + $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); + $data = [ + 'MerchantID' => $this->settings->merchantId, + 'Authority' => $authority, + 'Amount' => $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10), // convert to toman + ]; + + $client = new SoapClient($this->getVerificationUrl(), ['encoding' => 'UTF-8']); + $result = $client->PaymentVerification($data); + + $bodyResponse = $result->Status; + if ($bodyResponse != 100) { + throw new InvalidPaymentException($this->translateStatus($bodyResponse), $bodyResponse); + } + + return $this->createReceipt($result->RefID); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + public function createReceipt($referenceId) + { + return new Receipt('zarinpal', $referenceId); + } + + /** + * Retrieve purchase url + * + * @return string + */ + protected function getPurchaseUrl() : string + { + return $this->settings->sandboxApiPurchaseUrl; + } + + /** + * Retrieve Payment url + * + * @return string + */ + protected function getPaymentUrl() : string + { + return $this->settings->sandboxApiPaymentUrl; + } + + /** + * Retrieve verification url + * + * @return string + */ + protected function getVerificationUrl() : string + { + return $this->settings->sandboxApiVerificationUrl; + } + + /** + * Convert status to a readable message. + * + * @param $status + * + * @return mixed|string + */ + private function translateStatus($status) + { + $translations = [ + '100' => 'تراکنش با موفقیت انجام گردید', + '101' => 'عمليات پرداخت موفق بوده و قبلا عملیات وریفای تراكنش انجام شده است', + '-9' => 'خطای اعتبار سنجی', + '-10' => 'ای پی و يا مرچنت كد پذيرنده صحيح نمی باشد', + '-11' => 'مرچنت کد فعال نیست لطفا با تیم پشتیبانی ما تماس بگیرید', + '-12' => 'تلاش بیش از حد در یک بازه زمانی کوتاه', + '-15' => 'ترمینال شما به حالت تعلیق در آمده با تیم پشتیبانی تماس بگیرید', + '-16' => 'سطح تاييد پذيرنده پايين تر از سطح نقره ای می باشد', + '-30' => 'اجازه دسترسی به تسویه اشتراکی شناور ندارید', + '-31' => 'حساب بانکی تسویه را به پنل اضافه کنید مقادیر وارد شده برای تسهیم صحيح نمی باشد', + '-32' => 'مقادیر وارد شده برای تسهیم صحيح نمی باشد', + '-33' => 'درصد های وارد شده صحيح نمی باشد', + '-34' => 'مبلغ از کل تراکنش بیشتر است', + '-35' => 'تعداد افراد دریافت کننده تسهیم بیش از حد مجاز است', + '-40' => 'پارامترهای اضافی نامعتبر، expire_in معتبر نیست', + '-50' => 'مبلغ پرداخت شده با مقدار مبلغ در وریفای متفاوت است', + '-51' => 'پرداخت ناموفق', + '-52' => 'خطای غیر منتظره با پشتیبانی تماس بگیرید', + '-53' => 'اتوریتی برای این مرچنت کد نیست', + '-54' => 'اتوریتی نامعتبر است', + ]; + + $unknownError = 'خطای ناشناخته رخ داده است. در صورت کسر مبلغ از حساب حداکثر پس از 72 ساعت به حسابتان برمیگردد'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } +} diff --git a/src/Drivers/Zarinpal/Strategies/Zaringate.php b/src/Drivers/Zarinpal/Strategies/Zaringate.php new file mode 100644 index 0000000..898b18f --- /dev/null +++ b/src/Drivers/Zarinpal/Strategies/Zaringate.php @@ -0,0 +1,211 @@ +invoice($invoice); + $this->settings = (object) $settings; + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + if (!empty($this->invoice->getDetails()['description'])) { + $description = $this->invoice->getDetails()['description']; + } else { + $description = $this->settings->description; + } + + $mobile = !empty($this->invoice->getDetails()['mobile']) ? $this->invoice->getDetails()['mobile'] : ''; + $email = !empty($this->invoice->getDetails()['email']) ? $this->invoice->getDetails()['email'] : ''; + $amount = $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10); // convert to toman + + $data = array( + 'MerchantID' => $this->settings->merchantId, + 'Amount' => $amount, + 'CallbackURL' => $this->settings->callbackUrl, + 'Description' => $description, + 'Mobile' => $mobile, + 'Email' => $email, + 'AdditionalData' => $this->invoice->getDetails() + ); + + $client = new SoapClient($this->getPurchaseUrl(), ['encoding' => 'UTF-8']); + $result = $client->PaymentRequest($data); + + $bodyResponse = $result->Status; + if ($bodyResponse != 100 || empty($result->Authority)) { + throw new PurchaseFailedException($this->translateStatus($bodyResponse), $bodyResponse); + } + + $this->invoice->transactionId($result->Authority); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay() : RedirectionForm + { + $transactionId = $this->invoice->getTransactionId(); + $paymentUrl = $this->getPaymentUrl(); + + $payUrl = str_replace(':authority', $transactionId, $paymentUrl); + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \SoapFault + */ + public function verify() : ReceiptInterface + { + $amount = $this->invoice->getAmount() / ($this->settings->currency == 'T' ? 1 : 10); // convert to toman + $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); + + $data = [ + 'MerchantID' => $this->settings->merchantId, + 'Authority' => $authority, + 'Amount' => $amount, // convert to toman + ]; + + $client = new SoapClient($this->getVerificationUrl(), ['encoding' => 'UTF-8']); + $result = $client->PaymentVerification($data); + + $bodyResponse = $result->Status; + if ($bodyResponse != 100) { + throw new InvalidPaymentException($this->translateStatus($bodyResponse), $bodyResponse); + } + + return $this->createReceipt($result->RefID); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + public function createReceipt($referenceId) + { + return new Receipt('zarinpal', $referenceId); + } + + /** + * Retrieve purchase url + * + * @return string + */ + protected function getPurchaseUrl() : string + { + return $this->settings->zaringateApiPurchaseUrl; + } + + /** + * Retrieve Payment url + * + * @return string + */ + protected function getPaymentUrl() : string + { + return $this->settings->zaringateApiPaymentUrl; + } + + /** + * Retrieve verification url + * + * @return string + */ + protected function getVerificationUrl() : string + { + return $this->settings->zaringateApiVerificationUrl; + } + + /** + * Convert status to a readable message. + * + * @param $status + * + * @return mixed|string + */ + private function translateStatus($status) + { + $translations = [ + '100' => 'تراکنش با موفقیت انجام گردید', + '101' => 'عمليات پرداخت موفق بوده و قبلا عملیات وریفای تراكنش انجام شده است', + '-9' => 'خطای اعتبار سنجی', + '-10' => 'ای پی و يا مرچنت كد پذيرنده صحيح نمی باشد', + '-11' => 'مرچنت کد فعال نیست لطفا با تیم پشتیبانی ما تماس بگیرید', + '-12' => 'تلاش بیش از حد در یک بازه زمانی کوتاه', + '-15' => 'ترمینال شما به حالت تعلیق در آمده با تیم پشتیبانی تماس بگیرید', + '-16' => 'سطح تاييد پذيرنده پايين تر از سطح نقره ای می باشد', + '-30' => 'اجازه دسترسی به تسویه اشتراکی شناور ندارید', + '-31' => 'حساب بانکی تسویه را به پنل اضافه کنید مقادیر وارد شده برای تسهیم صحيح نمی باشد', + '-32' => 'مقادیر وارد شده برای تسهیم صحيح نمی باشد', + '-33' => 'درصد های وارد شده صحيح نمی باشد', + '-34' => 'مبلغ از کل تراکنش بیشتر است', + '-35' => 'تعداد افراد دریافت کننده تسهیم بیش از حد مجاز است', + '-40' => 'پارامترهای اضافی نامعتبر، expire_in معتبر نیست', + '-50' => 'مبلغ پرداخت شده با مقدار مبلغ در وریفای متفاوت است', + '-51' => 'پرداخت ناموفق', + '-52' => 'خطای غیر منتظره با پشتیبانی تماس بگیرید', + '-53' => 'اتوریتی برای این مرچنت کد نیست', + '-54' => 'اتوریتی نامعتبر است', + ]; + + $unknownError = 'خطای ناشناخته رخ داده است. در صورت کسر مبلغ از حساب حداکثر پس از 72 ساعت به حسابتان برمیگردد'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } +} diff --git a/src/Drivers/Zarinpal/Zarinpal.php b/src/Drivers/Zarinpal/Zarinpal.php index 9746395..9c9c1ba 100644 --- a/src/Drivers/Zarinpal/Zarinpal.php +++ b/src/Drivers/Zarinpal/Zarinpal.php @@ -3,16 +3,37 @@ namespace Shetabit\Multipay\Drivers\Zarinpal; use Shetabit\Multipay\Abstracts\Driver; +use Shetabit\Multipay\Contracts\DriverInterface; use Shetabit\Multipay\Exceptions\InvalidPaymentException; use Shetabit\Multipay\Exceptions\PurchaseFailedException; use Shetabit\Multipay\Contracts\ReceiptInterface; +use Shetabit\Multipay\Drivers\Zarinpal\Strategies\Normal; +use Shetabit\Multipay\Drivers\Zarinpal\Strategies\Sandbox; +use Shetabit\Multipay\Drivers\Zarinpal\Strategies\Zaringate; +use Shetabit\Multipay\Exceptions\DriverNotFoundException; use Shetabit\Multipay\Invoice; -use Shetabit\Multipay\Receipt; use Shetabit\Multipay\RedirectionForm; -use Shetabit\Multipay\Request; class Zarinpal extends Driver { + /** + * Strategies map. + * + * @var array + */ + public static $strategies = [ + 'normal' => Normal::class, + 'sandbox' => Sandbox::class, + 'zaringate' => Zaringate::class, + ]; + + /** + * Current strategy instance. + * + * @var DriverInterface $strategy + */ + protected $strategy; + /** * Invoice * @@ -36,8 +57,9 @@ class Zarinpal extends Driver */ public function __construct(Invoice $invoice, $settings) { - $this->invoice($invoice); + $this->invoice = $invoice; $this->settings = (object) $settings; + $this->strategy = $this->getFreshStrategyInstance($this->invoice, $this->settings); } /** @@ -50,43 +72,7 @@ public function __construct(Invoice $invoice, $settings) */ public function purchase() { - if (!empty($this->invoice->getDetails()['description'])) { - $description = $this->invoice->getDetails()['description']; - } else { - $description = $this->settings->description; - } - - if (!empty($this->invoice->getDetails()['mobile'])) { - $mobile = $this->invoice->getDetails()['mobile']; - } - - if (!empty($this->invoice->getDetails()['email'])) { - $email = $this->invoice->getDetails()['email']; - } - - $data = array( - 'MerchantID' => $this->settings->merchantId, - 'Amount' => $this->invoice->getAmount(), - 'CallbackURL' => $this->settings->callbackUrl, - 'Description' => $description, - 'Mobile' => $mobile ?? '', - 'Email' => $email ?? '', - 'AdditionalData' => $this->invoice->getDetails() - ); - - $client = new \SoapClient($this->getPurchaseUrl(), ['encoding' => 'UTF-8']); - $result = $client->PaymentRequest($data); - - if ($result->Status != 100 || empty($result->Authority)) { - // some error has happened - $message = $this->translateStatus($result->Status); - throw new PurchaseFailedException($message, $result->Status); - } - - $this->invoice->transactionId($result->Authority); - - // return the transaction's id - return $this->invoice->getTransactionId(); + return $this->strategy->purchase(); } /** @@ -96,16 +82,7 @@ public function purchase() */ public function pay() : RedirectionForm { - $transactionId = $this->invoice->getTransactionId(); - $paymentUrl = $this->getPaymentUrl(); - - if (strtolower($this->getMode()) == 'zaringate') { - $payUrl = str_replace(':authority', $transactionId, $paymentUrl); - } else { - $payUrl = $paymentUrl.$transactionId; - } - - return $this->redirectWithForm($payUrl, [], 'GET'); + return $this->strategy->pay(); } /** @@ -118,144 +95,35 @@ public function pay() : RedirectionForm */ public function verify() : ReceiptInterface { - $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); - $status = Request::input('Status'); - - $data = [ - 'MerchantID' => $this->settings->merchantId, - 'Authority' => $authority, - 'Amount' => $this->invoice->getAmount(), - ]; - - if ($status != 'OK') { - throw new InvalidPaymentException('عملیات پرداخت توسط کاربر لغو شد.', -22); - } - - $client = new \SoapClient($this->getVerificationUrl(), ['encoding' => 'UTF-8']); - $result = $client->PaymentVerification($data); - - if ($result->Status != 100) { - $message = $this->translateStatus($result->Status); - throw new InvalidPaymentException($message, $result->Status); - } - - return $this->createReceipt($result->RefID); + return $this->strategy->verify(); } /** - * Generate the payment's receipt + * Get zarinpal payment's strategy according to config's mode. * - * @param $referenceId - * - * @return Receipt - */ - public function createReceipt($referenceId) - { - return new Receipt('zarinpal', $referenceId); - } - - /** - * Convert status to a readable message. - * - * @param $status - * - * @return mixed|string - */ - private function translateStatus($status) - { - $translations = array( - "-1" => "اطلاعات ارسال شده ناقص است.", - "-2" => "IP و يا مرچنت كد پذيرنده صحيح نيست", - "-3" => "با توجه به محدوديت هاي شاپرك امكان پرداخت با رقم درخواست شده ميسر نمي باشد", - "-4" => "سطح تاييد پذيرنده پايين تر از سطح نقره اي است.", - "-11" => "درخواست مورد نظر يافت نشد.", - "-12" => "امكان ويرايش درخواست ميسر نمي باشد.", - "-21" => "هيچ نوع عمليات مالي براي اين تراكنش يافت نشد", - "-22" => "تراكنش نا موفق ميباشد", - "-33" => "رقم تراكنش با رقم پرداخت شده مطابقت ندارد", - "-34" => "سقف تقسيم تراكنش از لحاظ تعداد يا رقم عبور نموده است", - "-40" => "اجازه دسترسي به متد مربوطه وجود ندارد.", - "-41" => "اطلاعات ارسال شده مربوط به AdditionalData غيرمعتبر ميباشد.", - "-42" => "مدت زمان معتبر طول عمر شناسه پرداخت بايد بين 30 دقيه تا 45 روز مي باشد.", - "-54" => "درخواست مورد نظر آرشيو شده است", - "101" => "عمليات پرداخت موفق بوده و قبلا PaymentVerification تراكنش انجام شده است.", - ); - - $unknownError = 'خطای ناشناخته رخ داده است.'; - - return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; - } - - /** - * Retrieve purchase url - * - * @return string - */ - protected function getPurchaseUrl() : string - { - $mode = $this->getMode(); - - switch ($mode) { - case 'sandbox': - $url = $this->settings->sandboxApiPurchaseUrl; - break; - case 'zaringate': - $url = $this->settings->zaringateApiPurchaseUrl; - break; - default: // default: normal - $url = $this->settings->apiPurchaseUrl; - break; - } - - return $url; - } - - /** - * Retrieve Payment url - * - * @return string + * @param Invoice $invoice + * @param $settings + * @return DriverInterface */ - protected function getPaymentUrl() : string + protected function getFreshStrategyInstance($invoice, $settings) : DriverInterface { - $mode = $this->getMode(); + $strategy = static::$strategies[$this->getMode()] ?? null; - switch ($mode) { - case 'sandbox': - $url = $this->settings->sandboxApiPaymentUrl; - break; - case 'zaringate': - $url = $this->settings->zaringateApiPaymentUrl; - break; - default: // default: normal - $url = $this->settings->apiPaymentUrl; - break; + if (! $strategy) { + $this->strategyNotFound(); } - return $url; + return new $strategy($invoice, $settings); } - /** - * Retrieve verification url - * - * @return string - */ - protected function getVerificationUrl() : string + protected function strategyNotFound() { - $mode = $this->getMode(); - - switch ($mode) { - case 'sandbox': - $url = $this->settings->sandboxApiVerificationUrl; - break; - case 'zaringate': - $url = $this->settings->zaringateApiVerificationUrl; - break; - default: // default: normal - $url = $this->settings->apiVerificationUrl; - break; - } + $message = sprintf( + 'Zarinpal payment mode not found (check your settings), valid modes are: %s', + implode(',', array_keys(static::$strategies)) + ); - return $url; + throw new DriverNotFoundException($message); } /** diff --git a/src/Drivers/Zibal/Zibal.php b/src/Drivers/Zibal/Zibal.php index be21662..627458e 100644 --- a/src/Drivers/Zibal/Zibal.php +++ b/src/Drivers/Zibal/Zibal.php @@ -60,8 +60,7 @@ public function purchase() { $details = $this->invoice->getDetails(); - // convert to toman - $toman = $this->invoice->getAmount() * 10; + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial $orderId = crc32($this->invoice->getUuid()).time(); if (!empty($details['orderId'])) { @@ -70,21 +69,16 @@ public function purchase() $orderId = $details['order_id']; } - $mobile = null; - if (!empty($details['mobile'])) { - $mobile = $details['mobile']; - } elseif (!empty($details['phone'])) { - $mobile = $details['phone']; - } - $data = array( "merchant"=> $this->settings->merchantId, //required "callbackUrl"=> $this->settings->callbackUrl, //required - "amount"=> $toman, //required + "amount"=> $amount, //required "orderId"=> $orderId, //optional - 'mobile' => $mobile, //optional for mpg ); + // Pass current $data array to add existing optional details + $data = $this->checkOptionalDetails($data); + $response = $this->client->request( 'POST', $this->settings->apiPurchaseUrl, @@ -131,11 +125,12 @@ public function pay() : RedirectionForm public function verify() : ReceiptInterface { $successFlag = Request::input('success'); + $status = Request::input('status'); $orderId = Request::input('orderId'); $transactionId = $this->invoice->getTransactionId() ?? Request::input('trackId'); if ($successFlag != 1) { - $this->notVerified('پرداخت با شکست مواجه شد'); + $this->notVerified($this->translateStatus($status), $status); } //start verfication @@ -153,7 +148,7 @@ public function verify() : ReceiptInterface $body = json_decode($response->getBody()->getContents(), false); if ($body->result != 100) { - $this->notVerified($body->message); + $this->notVerified($body->message, $body->result); } /* @@ -161,7 +156,7 @@ public function verify() : ReceiptInterface var_dump($body); */ - return $this->createReceipt($orderId); + return $this->createReceipt($body->refNumber); } /** @@ -178,18 +173,97 @@ protected function createReceipt($referenceId) return $receipt; } + private function translateStatus($status) + { + $translations = [ + -2 => 'خطای داخلی', + -1 => 'در انتظار پردخت', + 2 => 'پرداخت شده - تاییدنشده', + 3 => 'تراکنش توسط کاربر لغو شد.', + 4 => 'شماره کارت نامعتبر می‌باشد.', + 5 => 'موجودی حساب کافی نمی‌باشد.', + 6 => 'رمز واردشده اشتباه می‌باشد.', + 7 => 'تعداد درخواست‌ها بیش از حد مجاز می‌باشد.', + 8 => 'تعداد پرداخت اینترنتی روزانه بیش از حد مجاز می‌باشد.', + 9 => 'مبلغ پرداخت اینترنتی روزانه بیش از حد مجاز می‌باشد.', + 10 => 'صادرکننده‌ی کارت نامعتبر می‌باشد.', + 11 => '‌خطای سوییچ', + 12 => 'کارت قابل دسترسی نمی‌باشد.' + ]; + + $unknownError = 'خطای ناشناخته ای رخ داده است.'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } + /** * Trigger an exception * * @param $message * @throws InvalidPaymentException */ - private function notVerified($message) + private function notVerified($message, $code = 0) { if (empty($message)) { - throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.'); + throw new InvalidPaymentException('خطای ناشناخته ای رخ داده است.', $code); } else { - throw new InvalidPaymentException($message); + throw new InvalidPaymentException($message, $code); } } + + /** + * Retrieve data from details using its name. + * + * @return string + */ + private function extractDetails($name) + { + $detail = null; + if (!empty($this->invoice->getDetails()[$name])) { + $detail = $this->invoice->getDetails()[$name]; + } elseif (!empty($this->settings->$name)) { + $detail = $this->settings->$name; + } + + return $detail; + } + + /** + * Checks optional parameters existence (except orderId) and + * adds them to the given $data array and returns new array + * with optional parameters for api call. + * + * To avoid errors and have a cleaner api call log, `null` + * parameters are not sent. + * + * To add new parameter support in the future, all that + * is needed is to add parameter name to $optionalParameters + * array. + * + * @param $data + * + * @return array + */ + private function checkOptionalDetails($data) + { + $optionalParameters = [ + 'mobile', + 'description', + 'allowedCards', + 'feeMode', + 'percentMode', + 'multiplexingInfos' + ]; + + foreach ($optionalParameters as $parameter) { + if (!is_null($this->extractDetails($parameter))) { + $parameterArray = array( + $parameter => $this->extractDetails($parameter) + ); + $data = array_merge($data, $parameterArray); + } + } + + return $data; + } } diff --git a/src/EventRegistrar.php b/src/EventEmitter.php similarity index 74% rename from src/EventRegistrar.php rename to src/EventEmitter.php index b1a8932..e8a71ab 100644 --- a/src/EventRegistrar.php +++ b/src/EventEmitter.php @@ -2,7 +2,7 @@ namespace Shetabit\Multipay; -class EventRegistrar +class EventEmitter { /** * List of listeners. @@ -41,16 +41,22 @@ public function addEventListener(string $event, callable $listener) */ public function removeEventListener(string $event, callable $listener = null) { - if (!empty($this->listeners[$event])) { - if (empty($listener)) { // remove the event and all of its listeners - unset($this->listeners[$event]); - } else { // remove only the given listener if exists - $listenerIndex = array_search($listener, $this->listeners[$event]); + if (empty($this->listeners[$event])) { + return; + } + + // remove the event and all of its listeners + if (empty($listener)) { + unset($this->listeners[$event]); + + return; + } + + // remove only the given listener if exists + $listenerIndex = array_search($listener, $this->listeners[$event]); - if ($listenerIndex !== false) { - unset($this->listeners[$event][$listenerIndex]); - } - } + if ($listenerIndex !== false) { + unset($this->listeners[$event][$listenerIndex]); } } @@ -70,7 +76,7 @@ public function dispatch(string $event, ...$arguments) return; } - array_walk($listeners[$event], function($listener) use ($arguments) { + array_walk($listeners[$event], function ($listener) use ($arguments) { call_user_func_array($listener, $arguments); }); } @@ -87,4 +93,4 @@ public function __call($name, $arguments) { $this->dispatch($name, $arguments); } -} \ No newline at end of file +} diff --git a/src/Exceptions/PurchaseFailedException.php b/src/Exceptions/PurchaseFailedException.php index 185c8ef..1d97b33 100644 --- a/src/Exceptions/PurchaseFailedException.php +++ b/src/Exceptions/PurchaseFailedException.php @@ -2,7 +2,9 @@ namespace Shetabit\Multipay\Exceptions; -class PurchaseFailedException extends \Exception +use Exception; + +class PurchaseFailedException extends Exception { // } diff --git a/src/Invoice.php b/src/Invoice.php index 8378431..9445738 100644 --- a/src/Invoice.php +++ b/src/Invoice.php @@ -19,7 +19,7 @@ class Invoice /** * Amount * - * @var int + * @var int|float */ protected $amount = 0; @@ -59,6 +59,7 @@ public function uuid($uuid = null) } $this->uuid = $uuid; + return $this; } /** @@ -82,8 +83,8 @@ public function getUuid() */ public function amount($amount) { - if (!is_int($amount)) { - throw new \Exception('Amount value should be an integer.'); + if (!is_numeric($amount)) { + throw new \Exception('Amount value should be a number (integer or float).'); } $this->amount = $amount; @@ -93,7 +94,7 @@ public function amount($amount) /** * Get the value of invoice * - * @return int + * @return int|float */ public function getAmount() { diff --git a/src/Payment.php b/src/Payment.php index a7a82c0..788ec44 100644 --- a/src/Payment.php +++ b/src/Payment.php @@ -6,9 +6,14 @@ use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Exceptions\DriverNotFoundException; use Shetabit\Multipay\Exceptions\InvoiceNotFoundException; +use Shetabit\Multipay\Traits\HasPaymentEvents; +use Shetabit\Multipay\Traits\InteractsWithRedirectionForm; class Payment { + use InteractsWithRedirectionForm; + use HasPaymentEvents; + /** * Payment Configuration. * @@ -49,13 +54,6 @@ class Payment */ protected $invoice; - /** - * Event registerar. - * - * @var EventRegistrar - */ - private static $eventRegistrar; - /** * PaymentManager constructor. * @@ -63,9 +61,9 @@ class Payment * * @throws \Exception */ - public function __construct($config = []) + public function __construct(array $config = []) { - $this->config = empty($config) ? self::loadDefaultConfig() : $config; + $this->config = empty($config) ? $this->loadDefaultConfig() : $config; $this->invoice(new Invoice()); $this->via($this->config['default']); } @@ -77,17 +75,7 @@ public function __construct($config = []) */ public static function getDefaultConfigPath() : string { - return "../config/payment.php"; - } - - /** - * Retrieve default config. - * - * @return array - */ - public static function loadDefaultConfig() : array - { - return require(self::getDefaultConfigPath()); + return dirname(__DIR__).'/config/payment.php'; } /** @@ -122,7 +110,7 @@ public function config($key, $value = null) */ public function callbackUrl($url = null) { - $this->callbackUrl = $url; + $this->config('callbackUrl', $url); return $this; } @@ -197,7 +185,7 @@ public function via($driver) $this->driver = $driver; $this->validateDriver(); $this->invoice->via($driver); - $this->settings = $this->config['drivers'][$driver]; + $this->settings = array_merge($this->loadDefaultConfig()['drivers'][$driver] ?? [], $this->config['drivers'][$driver]); return $this; } @@ -296,116 +284,13 @@ public function verify($finalizeCallback = null) : ReceiptInterface } /** - * Add verification event listener. - * - * @param callable $listener - * - * @return void - */ - public static function addPurchaseListener(callable $listener) - { - self::singletoneEventRegistrar(); - - self::$eventRegistrar->addEventListener('purchase', $listener); - } - - /** - * Remove verification event listener. - * - * @param callable|null $listener - * - * @return void - */ - public static function removePurchaseListener(callable $listener = null) - { - self::singletoneEventRegistrar(); - - self::$eventRegistrar->removeEventListener('purchase', $listener); - } - - /** - * Add pay event listener. - * - * @param callable $listener - * - * @return void - */ - public static function addPayListener(callable $listener) - { - self::singletoneEventRegistrar(); - - self::$eventRegistrar->addEventListener('pay', $listener); - } - - /** - * Remove pay event listener. - * - * @param callable|null $listener - * - * @return void - */ - public static function removePayListener(callable $listener = null) - { - self::singletoneEventRegistrar(); - - self::$eventRegistrar->removeEventListener('pay', $listener); - } - - /** - * Add verification event listener. - * - * @param callable $listener - * - * @return void - */ - public static function addVerifyListener(callable $listener) - { - self::singletoneEventRegistrar(); - - self::$eventRegistrar->addEventListener('verify', $listener); - } - - /** - * Remove verification event listener. - * - * @param callable|null $listener - * - * @return void - */ - public static function removeVerifyListener(callable $listener = null) - { - self::singletoneEventRegistrar(); - - self::$eventRegistrar->removeEventListener('verify', $listener); - } - - /** - * Dispatch an event. - * - * @param string $event - * @param array ...$arguments - * - * @return void - */ - protected function dispatchEvent(string $event, array ...$arguments) - { - self::singletoneEventRegistrar(); - - self::$eventRegistrar->dispatch($event, ...$arguments); - } - - /** - * Add an singletone event registerar. + * Retrieve default config. * - * @return void + * @return array */ - protected static function singletoneEventRegistrar() + protected function loadDefaultConfig() : array { - if (static::$eventRegistrar instanceof EventRegistrar) { - return; - } - - static::$eventRegistrar = new EventRegistrar; + return require(static::getDefaultConfigPath()); } /** diff --git a/src/Receipt.php b/src/Receipt.php index ba78ed5..1ca1a5b 100644 --- a/src/Receipt.php +++ b/src/Receipt.php @@ -27,6 +27,6 @@ public function __set($name, $value) */ public function __get($name) { - $this->getDetail($name); + return $this->getDetail($name); } } diff --git a/src/RedirectionForm.php b/src/RedirectionForm.php index e4e9ce5..a0ebf9a 100644 --- a/src/RedirectionForm.php +++ b/src/RedirectionForm.php @@ -2,6 +2,7 @@ namespace Shetabit\Multipay; +use Exception; use JsonSerializable; class RedirectionForm implements JsonSerializable @@ -12,7 +13,7 @@ class RedirectionForm implements JsonSerializable * @var string */ protected $method = 'POST'; - + /** * Form's inputs * @@ -34,7 +35,21 @@ class RedirectionForm implements JsonSerializable */ protected static $viewPath; - public function __construct($action, array $inputs = [], $method = 'POST') + /** + * The callable function that renders the given view + * + * @var callable + */ + protected static $viewRenderer; + + /** + * Redirection form constructor. + * + * @param string $action + * @param array $inputs + * @param string $method + */ + public function __construct(string $action, array $inputs = [], string $method = 'POST') { $this->action = $action; $this->inputs = $inputs; @@ -48,7 +63,7 @@ public function __construct($action, array $inputs = [], $method = 'POST') */ public static function getDefaultViewPath() : string { - return '../config/payment.php'; + return dirname(__DIR__).'/resources/views/redirect-form.php'; } /** @@ -60,7 +75,7 @@ public static function getDefaultViewPath() : string */ public static function setViewPath(string $path) { - self::$viewPath = $path; + static::$viewPath = $path; } /** @@ -70,7 +85,33 @@ public static function setViewPath(string $path) */ public static function getViewPath() : string { - return self::$viewPath ?? self::getDefaultViewPath(); + return static::$viewPath ?? static::getDefaultViewPath(); + } + + /** + * Set view renderer + * + * @param callable $renderer + */ + public static function setViewRenderer(callable $renderer) + { + static::$viewRenderer = $renderer; + } + + /** + * Retrieve default view renderer. + * + * @return callable + */ + protected function getDefaultViewRenderer() : callable + { + return function (string $view, string $action, array $inputs, string $method) { + ob_start(); + + require($view); + + return ob_get_clean(); + }; } /** @@ -107,7 +148,7 @@ public function getAction() : string * Alias for getAction method. * * @alias getAction - * + * * @return string */ public function getUrl() : string @@ -118,29 +159,20 @@ public function getUrl() : string /** * Render form. * - * @param string $action - * @param array $inputs - * @param string $method - * * @return string */ public function render() : string { $data = [ - 'method' => $this->getMethod(), - 'inputs' => $this->getInputs(), - 'action' => $this->getAction(), + "view" => static::getViewPath(), + "action" => $this->getAction(), + "inputs" => $this->getInputs(), + "method" => $this->getMethod(), ]; - ob_start(); + $renderer = is_callable(static::$viewRenderer) ? static::$viewRenderer : $this->getDefaultViewRenderer(); - extract($data); - - $viewPath = self::getViewPath(); - - require($viewPath); - - return ob_get_clean(); + return call_user_func_array($renderer, $data); } /** @@ -148,11 +180,20 @@ public function render() : string * * @param $options * + * @throws Exception * @return string */ - public function toJson($options = JSON_UNESCAPED_UNICODE) : string + public function toJson($options = JSON_UNESCAPED_UNICODE) { - return json_encode($this, $options); + $this->sendJsonHeader(); + + $json = json_encode($this, $options); + + if (json_last_error() != JSON_ERROR_NONE) { + throw new Exception(json_last_error_msg()); + } + + return $json; } /** @@ -168,15 +209,15 @@ public function toString() : string /** * Serialize to json * - * @return mixed + * @return array */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return [ - 'method' => $this->getMethod(), - 'inputs' => $this->getInputs(), 'action' => $this->getAction(), - 'view' => $this->getViewPath(), + 'inputs' => $this->getInputs(), + 'method' => $this->getMethod(), ]; } @@ -189,4 +230,14 @@ public function __toString() { return $this->toString(); } -} \ No newline at end of file + + /** + * Send application/json header + * + * @return void + */ + private function sendJsonHeader() + { + header('Content-Type: application/json'); + } +} diff --git a/src/Request.php b/src/Request.php index 2ff6884..a6fba50 100644 --- a/src/Request.php +++ b/src/Request.php @@ -25,14 +25,20 @@ class Request */ protected $getData = []; + /** + * Overwritten methods + * @var array + */ + protected static $overwrittenMethods = []; + /** * Request constructor. */ public function __construct() { - $this->request = $_REQUEST; - $this->post = $_POST; - $this->get = $_GET; + $this->requestData = $_REQUEST; + $this->postData = $_POST; + $this->getData = $_GET; } /** @@ -42,9 +48,13 @@ public function __construct() * * @return mixed|null */ - public function input(string $name) + public static function input(string $name) { - return $this->requestData[$name] ?? null; + if (isset(static::$overwrittenMethods['input'])) { + return (static::$overwrittenMethods['input'])($name); + } + + return (new static)->requestData[$name] ?? null; } /** @@ -54,9 +64,13 @@ public function input(string $name) * * @return void */ - public function post(string $name) + public static function post(string $name) { - return $this->postData[$name] ?? null; + if (isset(static::$overwrittenMethods['post'])) { + return (static::$overwrittenMethods['post'])($name); + } + + return (new static)->postData[$name] ?? null; } /** @@ -66,21 +80,21 @@ public function post(string $name) * * @return void */ - public function get(string $name) + public static function get(string $name) { - return $this->getData[$name] ?? null; + if (isset(static::$overwrittenMethods['get'])) { + return (static::$overwrittenMethods['get'])($name); + } + + return (new static)->getData[$name] ?? null; } /** - * Call methods statically. - * - * @param string $name - * @param array $arguments - * - * @return mixed|null + * @param string $method + * @param $callback */ - public function __callStatic($name, $arguments) + public static function overwrite($method, $callback) { - return call_user_func_array([new static, $name], $arguments); + static::$overwrittenMethods[$method] = $callback; } -} \ No newline at end of file +} diff --git a/src/Traits/HasPaymentEvents.php b/src/Traits/HasPaymentEvents.php new file mode 100644 index 0000000..33de1db --- /dev/null +++ b/src/Traits/HasPaymentEvents.php @@ -0,0 +1,128 @@ +addEventListener('purchase', $listener); + } + + /** + * Remove verification event listener. + * + * @param callable|null $listener + * + * @return void + */ + public static function removePurchaseListener(callable $listener = null) + { + static::singletoneEventEmitter(); + + static::$eventEmitter->removeEventListener('purchase', $listener); + } + + /** + * Add pay event listener. + * + * @param callable $listener + * + * @return void + */ + public static function addPayListener(callable $listener) + { + static::singletoneEventEmitter(); + + static::$eventEmitter->addEventListener('pay', $listener); + } + + /** + * Remove pay event listener. + * + * @param callable|null $listener + * + * @return void + */ + public static function removePayListener(callable $listener = null) + { + static::singletoneEventEmitter(); + + static::$eventEmitter->removeEventListener('pay', $listener); + } + + /** + * Add verification event listener. + * + * @param callable $listener + * + * @return void + */ + public static function addVerifyListener(callable $listener) + { + static::singletoneEventEmitter(); + + static::$eventEmitter->addEventListener('verify', $listener); + } + + /** + * Remove verification event listener. + * + * @param callable|null $listener + * + * @return void + */ + public static function removeVerifyListener(callable $listener = null) + { + static::singletoneEventEmitter(); + + static::$eventEmitter->removeEventListener('verify', $listener); + } + + /** + * Dispatch an event. + * + * @param string $event + * @param ...$arguments + * + * @return void + */ + protected function dispatchEvent(string $event, ...$arguments) + { + static::singletoneEventEmitter(); + + static::$eventEmitter->dispatch($event, ...$arguments); + } + + /** + * Add an singletone event registerar. + * + * @return void + */ + protected static function singletoneEventEmitter() + { + if (static::$eventEmitter instanceof EventEmitter) { + return; + } + + static::$eventEmitter = new EventEmitter; + } +} diff --git a/src/Traits/InteractsWithRedirectionForm.php b/src/Traits/InteractsWithRedirectionForm.php new file mode 100644 index 0000000..e268610 --- /dev/null +++ b/src/Traits/InteractsWithRedirectionForm.php @@ -0,0 +1,50 @@ +invoice($invoice); + $this->settings=$settings; + } + + public function purchase() + { + return static::TRANSACTION_ID; + } + + public function pay(): RedirectionForm + { + return $this->redirectWithForm('/', [ + 'amount' => $this->invoice->getAmount() + ], 'GET'); + } + + public function verify(): ReceiptInterface + { + return new Receipt(static::DRIVER_NAME, static::REFERENCE_ID); + } +} diff --git a/tests/Mocks/MockPaymentManager.php b/tests/Mocks/MockPaymentManager.php new file mode 100644 index 0000000..8cae352 --- /dev/null +++ b/tests/Mocks/MockPaymentManager.php @@ -0,0 +1,33 @@ +driver; + } + + public function getConfig() : array + { + return $this->config; + } + + public function getCallbackUrl() : string + { + return $this->settings['callbackUrl']; + } + + public function getInvoice() + { + return $this->invoice; + } + + public function getCurrentDriverSetting() + { + return $this->settings; + } +} diff --git a/tests/PaymentTest.php b/tests/PaymentTest.php new file mode 100644 index 0000000..c411c3b --- /dev/null +++ b/tests/PaymentTest.php @@ -0,0 +1,139 @@ +config(); + $manager = $this->getManagerFreshInstance(); + + $this->assertEquals($config['default'], $manager->getDriver()); + } + + public function testItWontAcceptInvalidDriver() + { + $this->expectException(\Exception::class); + + $manager = $this->getManagerFreshInstance(); + $manager->via('none_existance_driver_name'); + } + + public function testConfigCanBeModified() + { + $manager = $this->getManagerFreshInstance(); + + $manager->config('foo', 'bar'); + + $config = $manager->getCurrentDriverSetting(); + + $this->assertArrayHasKey('foo', $config); + $this->assertSame('bar', $config['foo']); + } + + public function testCallbackUrlCanBeModified() + { + $manager = $this->getManagerFreshInstance(); + $manager->callbackUrl('/random_url'); + + $this->assertEquals('/random_url', $manager->getCallbackUrl()); + } + + public function testAmountCanBeSetted() + { + $amount = 10000; + $manager = $this->getManagerFreshInstance(); + $manager->amount($amount); + + $this->assertSame($amount, $manager->getInvoice()->getAmount()); + } + + public function testDeteilCanBeSetted() + { + $manager = $this->getManagerFreshInstance(); + + // array style + $manager->detail(['foo' => 'bar']); + + // normal style + $manager->detail('john', 'doe'); + + $invoice = $manager->getInvoice(); + + $this->assertEquals('bar', $invoice->getDetail('foo')); + $this->assertEquals('doe', $invoice->getDetail('john')); + } + + public function testDriverCanBeChanged() + { + $driverName = 'bar'; + $manager = $this->getManagerFreshInstance(); + $manager->via($driverName); + + $this->assertEquals($driverName, $manager->getDriver()); + } + + public function testPurchase() + { + $amount = 10000; + $manager = $this->getManagerFreshInstance(); + + $manager + ->via('bar') + ->amount($amount) + ->purchase(null, function ($driver, $transactionId) use ($amount) { + $this->assertEquals(BarDriver::TRANSACTION_ID, $transactionId); + $this->assertSame($amount, $driver->getInvoice()->getAmount()); + }); + } + + public function testCustomInvoiceCanBeUsedInPurchase() + { + $manager = $this->getManagerFreshInstance(); + + $invoice = new Invoice; + $invoice->amount(10000); + + $manager + ->via('bar') + ->purchase($invoice, function ($driver, $transactionId) use ($invoice) { + $this->assertEquals(BarDriver::TRANSACTION_ID, $transactionId); + $this->assertSame($invoice->getAmount(), $driver->getInvoice()->getAmount()); + }); + } + + public function testPay() + { + $amount = 10000; + $manager = $this->getManagerFreshInstance(); + + $redirectionForm = $manager->amount($amount)->via('bar')->purchase()->pay(); + $inputs = $redirectionForm->getInputs(); + + $this->assertInstanceOf(RedirectionForm::class, $redirectionForm); + $this->assertEquals($inputs['amount'], $amount); + } + + public function testVerify() + { + $amount = 10000; + $manager = $this->getManagerFreshInstance(); + + $receipt = $manager->amount($amount)->via('bar')->transactionId(BarDriver::TRANSACTION_ID)->verify(); + + $this->assertSame(BarDriver::REFERENCE_ID, $receipt->getReferenceId()); + $this->assertInstanceOf(Carbon::class, $receipt->getDate()); + } + + protected function getManagerFreshInstance() + { + return new MockPaymentManager($this->config()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index c776f2e..b0d23e3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,29 +2,35 @@ namespace Shetabit\Multipay\Tests; -use Orchestra\Testbench\TestCase as BaseTestCase; -use Shetabit\Multipay\Tests\Mocks\Drivers\BarDriver; +use PHPUnit\Framework\TestCase as BaseTestCase; +use Shetabit\Multipay\Tests\Drivers\BarDriver; class TestCase extends BaseTestCase { - protected function getPackageProviders($app) + private $config = []; + + protected function setUp() : void { - return ['Shetabit\Multipay\Provider\PaymentServiceProvider']; + $this->environmentSetUp(); } - protected function getPackageAliases($app) + protected function config() : array { - return [ - 'Sms' => 'Shetabit\Multipay\Facade\Payment', - ]; + return $this->config; } - protected function getEnvironmentSetUp($app) + private function environmentSetUp() { - $settings = require __DIR__.'/../src/Config/payment.php'; - $settings['drivers']['bar'] = ['key' => 'foo']; - $settings['map']['bar'] = BarDriver::class; + $this->config = $this->loadConfig(); + + $this->config['map']['bar'] = BarDriver::class; + $this->config['drivers']['bar'] = [ + 'callback' => '/callback' + ]; + } - $app['config']->set('payment', $settings); + private function loadConfig() : array + { + return require(__DIR__.'/../config/payment.php'); } } diff --git a/tests/Traits/DriverCommon.php b/tests/Traits/DriverCommon.php new file mode 100644 index 0000000..88b63ac --- /dev/null +++ b/tests/Traits/DriverCommon.php @@ -0,0 +1,77 @@ +driverInstance = $this->getDriverInstance(); + $this->transactionId = 'biSBUv86G'; + } + + /** + * Test Purchase method. + * + * @throws \Exception + */ + public function testPurchase() + { + $amount = 10000; + $detailKey = 'foo'; + $detailValue = 'bar'; + + $this + ->driverInstance + ->detail($detailKey, $detailValue) + ->purchase($amount, function ($driver, $transactionId) { + $this->assertEquals($this->amount, $driver->getInvoice()->getAmount()); + $this->assertEquals([$this->detailKey => $this->detailValue], $driver->getInvoice()->getDetails()); + $this->assertEquals($this->transactionId, $transactionId); + }); + } + + /** + * Test pay method. + * + * @throws \Exception + */ + public function testPay() + { + $amount = 1000; + + $this + ->driverInstance + ->amount($amount) + ->purchase(); + + $this->assertInstanceOf(RedirectionForm::class, $this->driverInstance); + } + + /** + * Test Verify method + * + * @throws \Shetabit\Multipay\Exceptions\InvoiceNotFoundException + */ + public function testVerify() + { + $amount = 1000; + + $receipt = $this + ->driverInstance + ->amount($amount) + ->transactionId($this->transactionId) + ->verify(); + + $this->assertInstanceOf(Carbon::class, $receipt->getDate()); + $this->assertEquals("test", $receipt->getDriver()); + $this->assertEquals("122156415036", $receipt->getReferenceId()); + } +}