diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitignore b/.gitignore index 35923b4..40c5728 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ phpunit.xml coverage coverage.xml .phpunit.result.cache +docker-compose.yml diff --git a/.styleci.yml b/.styleci.yml index 6cf98dc..0285f17 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,5 +1 @@ preset: laravel - -finder: - exclude: - - "tests" diff --git a/.travis.yml b/.travis.yml index 3e211a0..fc48cf3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,34 +7,37 @@ cache: matrix: fast_finish: true include: - - php: 7.2 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' - - php: 7.2 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + # Laravel 7 - php: 7.3 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' - - php: 7.3 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' - - php: 7.2 - env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' - - php: 7.2 - env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' - - php: 7.3 - env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' - php: 7.3 - env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' - - php: 7.2 + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' + + - php: 7.4 env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' - - php: 7.2 + - php: 7.4 env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' - - php: 7.3 + + - php: 8.0 env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' - - php: 7.3 + - php: 8.0 env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' + + # Laravel 8 + - php: 7.3 + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.3 + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.4 - env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' - php: 7.4 - env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' COVERAGE=true + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' COVERAGE=true + + - php: 8.0 + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 8.0 + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' before_install: - travis_retry composer self-update diff --git a/docs/changelog.md b/CHANGELOG.md similarity index 67% rename from docs/changelog.md rename to CHANGELOG.md index 9ca3a5a..1fd6ae4 100644 --- a/docs/changelog.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Laravel Toman will be documented in this file. +## Version 2.0 + +### Change +- Package interfaces are significantly changed + ## Version 1.0 ### Added diff --git a/docs/contributing.md b/CONTRIBUTING.md similarity index 65% rename from docs/contributing.md rename to CONTRIBUTING.md index de35553..436bd6b 100644 --- a/docs/contributing.md +++ b/CONTRIBUTING.md @@ -1,25 +1,23 @@ # Contributing -Contributions are welcome and will be fully credited. +Contributions are welcome and will be fully credited. They are accepted via Pull Requests on [Laravel Toman](https://github.com/evryn/laravel-toman) repository. -Contributions are accepted via Pull Requests on [Github](https://github.com/amirrezanasiri/laraveltoman). - -# Things you could do +# Things you can do If you want to contribute but do not know where to start, this list provides some starting points. - * Add callback fakers to simulate actual gateway callbacks. * Add a new Payment Gateway: * [YekPay.com](https://yekpay.com/) * [Pay.ir](https://pay.ir/) * [PayPing.ir](https://www.payping.ir/) * Refactor if you think there are better approach to do things. + * Improve documents and comments; Fix grammatical errors, typos, etc. ## Pull Requests -- **Add tests!** - Your patch won't be accepted if it doesn't have comprehensive tests. +- **Add tests** - We need to have tests for the features. - **Document any change in behaviour** - Make sure documents in `docs/` directory are kept up-to-date. -- **Consider our release cycle** - We're following [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. +- **Consider our release cycle** - We're following [SemVer v2.0.0](http://semver.org/). - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. diff --git a/license.md b/LICENSE.md similarity index 96% rename from license.md rename to LICENSE.md index 8e5c378..5e3a940 100644 --- a/license.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # MIT License -Copyright (c) 2019 Amirreza Nasiri +Copyright (c) Amirreza Nasiri Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/readme.md b/README.md similarity index 80% rename from readme.md rename to README.md index 75ecf80..e88588c 100644 --- a/readme.md +++ b/README.md @@ -11,7 +11,7 @@

Latest Version on Packagist Total Downloads - Build Status + Build Status Code Coverage StyleCI

@@ -22,12 +22,19 @@ and start using in <5m

+

+ داکیومنت فارسی + رو بخونین و تو 5 دقیقه استفاده‌اش کنین + 📚 +

+ # About Toman (تومَن) Toman is a Laravel package which makes working with popular payment gateways much easier. ## Supported Gateways ✅ [Zarinpal.com](https://zarinpal.com) +✅ [IDPay.ir](https://idpay.ir) 🔘 [YekPay.com](https://yekpay.com/) 🔘 [Pay.ir](https://pay.ir/) 🔘 [PayPing.ir](https://www.payping.ir/) @@ -35,17 +42,17 @@ Toman is a Laravel package which makes working with popular payment gateways muc ## Simple to use -Request new payment as easy as: +Request a new payment:

- Request new Payment + Request new Payment

-And simply verify callback: +And simply verify its callback:

- Verify Payment + Verify Payment

diff --git a/composer.json b/composer.json index 85f14bc..e0df7b6 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "evryn/laravel-toman", - "description": "A simple stable Laravel package to handle popular payment gateways in Iran including Zarinpal.", + "description": "A simple stable Laravel package to handle popular payment gateways in Iran including ZarinPal and IDPay.", "license": "MIT", "authors": [ { @@ -11,19 +11,20 @@ ], "homepage": "https://github.com/evryn/laravel-toman", "keywords": [ + "zarinpal", "زرین پال", + "idpay", "آیدی پی", "laravel", "laraveltoman", "laravel-toman", - "toman", "zarinpal", "زرین پال", "payment", - "gateway", "درگاه", "پرداخت" + "toman", "payment", "gateway", "درگاه", "پرداخت" ], "require": { - "php": "^7.2", - "illuminate/support": "~5.8.0|^6.0|^7.0", - "guzzlehttp/guzzle": "^6.3" + "php": "^7.3|^8.0", + "illuminate/support": "^7.0|^8.0", + "guzzlehttp/guzzle": "^6.3|^7.0" }, "require-dev": { "phpunit/phpunit": "^8.0", - "mockery/mockery": "^1.1", - "orchestra/testbench": "~3.8.0|^4.0|^5.0", + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "~3.8.0|^4.0|^5.0|^6.0", "sempro/phpunit-pretty-print": "^1.0" }, "autoload": { @@ -48,7 +49,7 @@ }, "scripts": { "test": "vendor/bin/phpunit --exclude-group external", - "test-coverage": "vendor/bin/phpunit --exclude-group external --coverage-clover=coverage.xml", - "test-dev": "vendor/bin/phpunit --configuration=phpunit.xml --coverage-html=coverage --coverage-text" + "test-coverage": "vendor/bin/phpunit --coverage-clover coverage.xml", + "test-dev": "vendor/bin/phpunit --coverage-html coverage --coverage-text" } } diff --git a/config/toman.php b/config/toman.php index f59b9ed..db66dc5 100644 --- a/config/toman.php +++ b/config/toman.php @@ -27,8 +27,8 @@ 'gateways' => [ 'zarinpal' => [ - // Use sandbox.zarinpal.com instead of zarinpal.com for testing - // purpose. Set it to false on production to receive real payments. + // Setting to true makes all payments happen in a testing environment to fake transactions. + // Set it to false on production to receive real payments. 'sandbox' => env('ZARINPAL_SANDBOX', false), // Merchant ID of your gateway provided by Zarinpal for your gateway @@ -36,8 +36,35 @@ 'merchant_id' => env('ZARINPAL_MERCHANT_ID'), ], + 'idpay' => [ + // Setting to true makes all payments happen in a testing environment to fake transactions. + // Set it to false on production to receive real payments. + 'sandbox' => env('IDPAY_SANDBOX', false), + + // API Key of your gateway provided by IDPay in your dashboard + // only. Looks like this: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + 'api_key' => env('IDPAY_API_KEY'), + ], + ], + /* + |-------------------------------------------------------------------------- + | Default Currency + |-------------------------------------------------------------------------- + | + Gateways accept different currencies; So in order to avoid confusion + | when switching between gateways, you can specify your intended currency + | here, and Toman will convert to proper one automatically. + | You can of course override it and specify another currency during making + | requests using Money object too. + | + | Supported currencies: "toman", "rial" + | + */ + + 'currency' => 'toman', + /* |-------------------------------------------------------------------------- | Default Description diff --git a/docker-compose.yml.example b/docker-compose.yml.example new file mode 100644 index 0000000..b2c1361 --- /dev/null +++ b/docker-compose.yml.example @@ -0,0 +1,19 @@ +version: "3.4" + +services: + php: + image: php:7.3-alpine + volumes: + - .:/var/www/html + + composer: + image: composer:2 + volumes: + - .:/app + - composer-cache:/tmp/cache + working_dir: /app + command: '-V' + +volumes: + composer-cache: + name: composer-cache diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7d82a33 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,40 @@ +# About Toman (تومَن) +Toman is a Laravel package that makes working with popular payment gateways much easier. + +There are dozens of gateway handlers; Here is why you may choose Laravel Toman: + +## Heavily Tested + + Build Status + + + Code Coverage + + +Payment is a critical topic, and its handlers must be tested from all aspects. Laravel Toman has an automated test suit with 100% coverage. On every released build, we're forced to pass all tests so you can add a payment to your code with confidence. + +## Elegant API + +APIs are pretty much easy to use! Requesting a new payment and verifying it will add ~10 lines to your codebase! 😎 + +## Supports Multiple Gateways + +We are planning to add new gateway providers. They'll require minimum possible changes in your code. +Currently, following gateways are available to use: +✅ [Zarinpal.com](https://zarinpal.com) +✅ [IDPay.ir](https://idpay.ir) +🔘 [YekPay.com](https://yekpay.com/) +🔘 [Pay.ir](https://pay.ir/) +🔘 [PayPing.ir](https://www.payping.ir/) +🔘 ... + +## Easy to Test + +Writing a test for your application and want to see if you're using the package correctly? No problem at all! + +See our testing instructions in your payment gateway section! + +

+➡ Why don't you see yourself? [Quick Start](getting-started.md) + +➡ Looking for old documents? V1 Docs diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 35781de..20ddf75 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -1,7 +1,6 @@ - ![logo](_media/logo.png) -# Laravel Toman 1.0 +# Laravel Toman 2.0 > A painless payment handler! @@ -13,8 +12,8 @@ Elegant API • Heavily Tested • Multiple Gateways Total Downloads - - Build Status + + Build Status Code Coverage @@ -25,3 +24,7 @@ Elegant API • Heavily Tested • Multiple Gateways [GitHub](https://github.com/evryn/laravel-toman) [Get Started](#quickstart) + + + +![color](#b3ffe9) diff --git a/docs/_media/logo.png b/docs/_media/logo.png index 9dad305..874bf0b 100644 Binary files a/docs/_media/logo.png and b/docs/_media/logo.png differ diff --git a/docs/_media/payment-request.png b/docs/_media/payment-request.png new file mode 100644 index 0000000..8f691d0 Binary files /dev/null and b/docs/_media/payment-request.png differ diff --git a/docs/_media/payment-verification.png b/docs/_media/payment-verification.png new file mode 100644 index 0000000..7a5a1ba Binary files /dev/null and b/docs/_media/payment-verification.png differ diff --git a/docs/_navbar.md b/docs/_navbar.md new file mode 100644 index 0000000..7531ae5 --- /dev/null +++ b/docs/_navbar.md @@ -0,0 +1,2 @@ +* [English](/) +* [فارسی](/fa/) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index d476b9c..489891a 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,18 +1,13 @@ -* [Introduction](introduction.md) +* [Introduction](README.md) -* Getting started - - * [Quick Start](quickstart.md) - * [Configuration](configuration.md) - * [Translations](translations.md) +* [Getting Started](getting-started.md) -* Available Gateways +* Payment Gateways - * [Zarinpal](gateways/zarinpal.md) - -* [Testing](testing.md) + * [💳 Zarinpal](gateways/zarinpal.md) + * [💳 IDPay](gateways/idpay.md) -* [Contributing](contributing.md) -* [Changelog](changelog.md) -* [Support 💜](support.md) +* [Contributing](../CONTRIBUTING.md) +* [Changelog](../CHANGELOG.md) +* [💜 Support](support.md) diff --git a/docs/fa/README.md b/docs/fa/README.md new file mode 100644 index 0000000..623f285 --- /dev/null +++ b/docs/fa/README.md @@ -0,0 +1,41 @@ +# درباره Toman (تومَن) +تومَن یه پکیج لاراوله که کار کردن با درگاه‌های پرداخت رو خیلی راحت‌تر می‌کنه. +کلی پکیج دیگه واسه هندل کردن درگاه‌ها وجود داره اما این پکیج ممکنه واستون مناسب‌تر باشه، چون: + +## قابل اطمینان + + Build Status + + + Code Coverage + + +پرداخت موضوع مهمیه و هندلرها باید از جنبه‌هاش مختلف تست شده باشن. +Laravel Toman یه تست سوئیت کامل داره. قبل از ریلیز کردن هر نسخه‌ای، مجبوریم که همه تست‌های خودکار رو پاس کنیم تا به دست شما برسه +و بتونین با خیال راحت تو کدهاتون استفاده کنین. + +## رابط‌های خوشگل + +رابط‌های این پکیج خیلی ساده‌تر از اونی هست که فکر می‌کنین. ایجاد پرداخت جدید و راستی‌آزماییش فقط 10 خط به کدبیس‌تون اضافه می‌کنه 😎 + +## پشتیبانی از چندین درگاه + +برنامه داریم که درگاه‌های جدید رو هم به تومن اضافه کنیم. قول می‌دیم که سوئیچ کردن رو درگاه‌های مختلف، کمترین تغییرات رو توی کدهاتون داشته باشه. +فعلاً این درگاه‌ها قابل استفاده هستن: + +✅ [Zarinpal.com](https://zarinpal.com) +✅ [IDPay.ir](https://idpay.ir) +🔘 [YekPay.com](https://yekpay.com/) +🔘 [Pay.ir](https://pay.ir/) +🔘 [PayPing.ir](https://www.payping.ir/) +🔘 ... + +## سادگی در تست کردن + +دارین برای کدهاتون تست سوئیت خودکار ایجاد می‌کنین و می‌خواین ببینین که از این پکیج هم درست استفاده می‌کنین یا نه؟ +مشکلی نیست! تو صفحه راهنمای درگاه‌ها، می‌تونین ببینین چطور به راحتی می‌شه تست کرد این جنبه از نرم‌افزارتون رو. + +

+➡ [بریم سراغ نصب!](getting-started.md) + +➡ دنبال داکیومنت قدیمی هستین؟ داکیومنت‌های نسخه 1 diff --git a/docs/fa/_coverpage.md b/docs/fa/_coverpage.md new file mode 100644 index 0000000..3a0cd96 --- /dev/null +++ b/docs/fa/_coverpage.md @@ -0,0 +1,30 @@ +![logo](../_media/logo.png) + +# Laravel Toman 2.0 + +> درگاه پرداخت - مثل آب خوردن! + +استفاده آسان • تست شده • پشتیبانی از چندین درگاه + + + Latest Version on Packagist + + + Total Downloads + + + Build Status + + + Code Coverage + + + StyleCI + + +[گیت‌هاب](https://github.com/evryn/laravel-toman) +[شروع به کار](#quickstart) + + + +![color](#b3ffe9) diff --git a/docs/fa/_navbar.md b/docs/fa/_navbar.md new file mode 100644 index 0000000..36e4749 --- /dev/null +++ b/docs/fa/_navbar.md @@ -0,0 +1,2 @@ +* [فارسی](/fa/) +* [English](/) diff --git a/docs/fa/_sidebar.md b/docs/fa/_sidebar.md new file mode 100644 index 0000000..c7f9afb --- /dev/null +++ b/docs/fa/_sidebar.md @@ -0,0 +1,13 @@ +* [معرفی](fa/README.md) + +* [شروع به کار](fa/getting-started.md) + +* درگاه‌های پرداخت + + * [💳 زرین‌پال](fa/gateways/zarinpal.md) + * [💳 آی‌دی پِی](fa/gateways/idpay.md) + +* [مشارکت](../CONTRIBUTING.md) +* [لیست تغییرات](../CHANGELOG.md) +* [💜 حمایت از ما](fa/support.md) + diff --git a/docs/fa/gateways/idpay.md b/docs/fa/gateways/idpay.md new file mode 100644 index 0000000..e211950 --- /dev/null +++ b/docs/fa/gateways/idpay.md @@ -0,0 +1,268 @@ +# استفاده از درگاه IDPay + +تومن درگاه [IDPay.ir](https://idpay.ir) رو بر اساس نسخه 1.1 [داکیومنت رسمی‌شون](https://idpay.ir/web-service/v1.1/) ساخته. + +## شروع به کار +### تنظیمات + +درگاه آی‌دی پِی نیاز به تغییر این مقادیر تو فایل `.env` داره: + +| متغیر محیطی | توضیحات | +|---------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TOMAN_GATEWAY` | (**اجباری**)
باید `idpay` باشه. | +| `IDPAY_API_KEY` | (**اجباری**)
API Key درگاه پرداخت که می‌تونین از پنل ارائه‌دهنده بگیرین.
نمونه: `0bcf346fc-3a79-4b36-b936-5ccbc2be0696` | +| `IDPAY_SANDBOX` | (اختیاری. مقدار پیش‌فرض: `false`)
اگه تنظیم شه به `true`، همه درخواست‌ها تو محیط تست این درگاه بدون پرداخت واقعی انجام می‌شه. این شرایط تو محیط توسعه لوکال به درد می‌خوره. + +نمونه: +```dotenv +... + +TOMAN_GATEWAY=idpay +IDPAY_API_KEY=0bcf346fc-3a79-4b36-b936-5ccbc2be0696 +``` + +#### واحد پولی + +واحد پولی رو می‌تونین به دو صورت معین کنین: + +**استفاده از فایل تنظیمات (کانفیگ):** + +| متد | تنظیم | یعنی | +|------------------|----------------------------|--------------| +| `amount(10000)` | `toman.currency = 'toman'` | 10,000 تومان | +| `amount(10000)` | `toman.currency = 'rial'` | 1,000 تومان | + +**صریحاً مشخص کردن:** +```php +use Evryn\LaravelToman\Money; + +...->amount(Money::Rial(10000)); +...->amount(Money::Toman(1000)); +``` + +## ⚡ درخواست پرداخت جدید + +```php +use Evryn\LaravelToman\Facades\Toman; + +// ... + +$request = Toman::orderId('order_1500') + ->amount(15000) + // ->description('Subscribing to Plan A') + // ->callback(route('payment.callback')) + // ->mobile('09350000000') + // ->email('amirreza@example.com') + // ->name('Amirreza Nasiri') + ->request(); + +if ($request->successful()) { + $transactionId = $request->transactionId(); + // Store created transaction details for verification + + return $request->pay(); // Redirect to payment URL +} + +if ($request->failed()) { + // Handle transaction request failure; Probably showing proper error to user. +} +``` + +متدهای قابل استفاده برای ایجاد درخواست پرداخت جدید با کلاس نمایه‌ای `Toman`: + +| متد | توضیحات | +|------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `amount($amount)` | **(الزامی)** تنظیم مبلغ قابل پرداخت. این درگاه واحد پولیش ریاله. | +| `orderId($orderId)` | **(الزامی)** تنظیم شناسه سفارش. این شناسه یه رشته هست که سمت شما به صورت یکتا باید ساخته بشه و موقع تایید پرداخت ازش استفاده بشه. | +| `callback($url)` | تنظیم یک آدرس URL کامل به عنوان Callback URL. بر کانفیگ `callback_route` اولویت داره. | +| `description($string)` | تنظیم توضیحات پرداخت. بر کانفیگ `description` اولویت داره. | +| `mobile($mobile)` | تنظیم شماره موبایل پرداخت‌کننده. | +| `email($email)` | تنظیم ایمیل پرداخت‌کننده. | +| `name($name)` | تنظیم نام پرداخت‌کننده. | +| `request()` | ارسال درخواست پرداخت جدید و بازگردوندن یک آبجکت از نوع `RequestedPayment`. | + + +استفاده از `RequestedPayment` برگردونده شده: + +|
متد
| توضیحات | +|-------------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `successful()` | درخواست پرداخت موفق بوده؛ شناسه تراکنش در دسترسه و می‌شه کاربر رو به صفحه پرداخت هدایت کرد. | +| `transactionId()` | [در صورت موفقیت] دریافت شناسه تراکنش. | +| `pay($options = [])` | [در صورت موفقیت] ریدایرکت کردن کاربر به صفحه پرداخت از کنترلر. یه آبجکت `RedirectResponse` برمی‌گردونه. | +| `paymentUrl($options = [])` | [در صورت موفقیت] دریافت آدرس پراخت نهایی. | +| `failed()` | درخواست پرداخت شکست خورد؛ پیام‌های مناسب و Exception در دسترس هستن. | +| `messages()` | [در صورت شکست] دریافت آرایه‌ای از پیام‌های خطا | +| `message()` | [در صورت شکست] دریافت اولین پیام خطا. | +| `throw()` | [در صورت شکست] پرت کردن Exception متناسب با خطا. | + + + +## ⚡ تایید پرداخت + +مکانیزم تایید پرداخت باید در مسیر مربوط به Callback ارسال شده پیاده شود. این کنترلر رو در نظر بگیرین: + +```php +transactionId() and $request->orderId() to match the + // non-paid payment. Take care of Double Spending. + + $payment = $request->verify(); + + if ($payment->successful()) { + // Store the successful transaction details + $referenceId = $payment->referenceId(); + } + + if ($payment->alreadyVerified()) { + // ... + } + + if ($payment->failed()) { + // ... + } + } +} +``` + +متدهای قابل استفاده برای تایید پرداخت با `CallbackRequest` یا کلاس نمایه‌ای `Toman`: + +| متد | توضیحات | +|------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `orderId($orderId)` | تنظیم شناسه سفارش. `CallbackRequest` اینو خودش پر می‌کنه. | +| `transactionId($id)` | تنظیم شناسه تراکنش برای بررسی تایید پرداخت. `CallbackRequest` اینو خودش پر می‌کنه.| +| `verify()` |ارسال درخواست بررسی و تایید پرداخت. یه آبجکت `CheckedPayment` برمی‌گردونه. | + + +استفاده از `CheckedPayment` برگردونده شده: + +| متد | توضیحات | +|-------------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `orderId()` | دریافت شناسه سفارشی که تو درخواست ارسال شده بود. | +| `transactionId()` | دریافت شناسه تراکنشی که تو درخواست ارسال شده بود. | +| `successful()` | پرداخت موفق بوده و شناسه ارجاع در دسترسه. | +| `transactionId()` | [در صورت موفقیت] دریافت شناسه ارجاع. | +| `alreadyVerified()` | پرداخت قبلاً یه بار بررسی و تایید شده بود. شناسه ارجاع همچنان در دسترسه. | +| `failed()` | پرداخت شکست خورده؛ پیام‌های مناسب و Exception در دسترس هستن. | +| `messages()` | [در صورت شکست] دریافت آرایه‌ای از پیام‌های خطا | +| `message()` | [در صورت شکست] دریافت اولین پیام خطا. | +| `throw()` | [در صورت شکست] پرت کردن Exception متناسب با خطا. | + +
+ +## بیشتر + +### تایید پرداخت به صورت دستی +اگه نیاز داشتین بدون استفاده از `CallbackRequest` تایید یه پرداخت رو بررسی کنین، می‌تونین از `Toman` استفاده کنین: + +```php +use Evryn\LaravelToman\Facades\Toman; + +// ... + +$payment = Toman::transactionId('tid_123') + ->orderId('order_1000') + ->verify(); + +if ($payment->successful()) { + // Store the successful transaction details + $referenceId = $payment->referenceId(); +} + +if ($payment->alreadyVerified()) { + // ... +} + +if ($payment->failed()) { + // ... +} +``` + +### تست کردن درگاه IDPay +اگه که برای نرم‌افزارتون تست سوئیت خودکار می‌نویسین و می‌خواین ببینین که با پکیج به درستی تعامل داره یا نه، ادامه بدین. + +#### 🧪 تست درخواست پرداخت + +از `Toman::fakeRequest()` استفاده کنین تا یه نتیجه درخواست ایجاد پرداخت رو شبیه‌سازی کنین و بعد محتوای درخواست رو با `Toman::assertRequested()` مورد بررسی قرار بدین. + +```php +use Evryn\LaravelToman\Facades\Toman; +use Evryn\LaravelToman\Money; + +final class PaymentTest extends TestCase +{ + /** @test */ + public function requests_new_payment_with_proper_data() + { + // Stub a successful or failed payment request result + Toman::fakeRequest()->successful()->withTransactionId('tid_123'); + + // Toman::fakeRequest()->failed(); + + // Act with your app ... + + // Assert that you've correctly requested payment + Toman::assertRequested(function ($request) { + return $request->merchantId() === 'your-idpay-api-key' + && $request->callback() === route('callback-route') + && $request->amount()->is(Money::Toman(50000)); + }); + } +} +``` + +#### 🧪 تست بررسی و تایید پرداخت + +از `Toman::fakeVerification()` استفاده کنین تا یه نتیجه درخواست بررسی و تایید رو شبیه‌سازی کنین و بعد محتوای درخواست رو با `Toman::assertCheckedForVerification()` مورد بررسی قرار بدین. + +```php +use Evryn\LaravelToman\Facades\Toman; +use Evryn\LaravelToman\Money; + +final class PaymentTest extends TestCase +{ + /** @test */ + public function verifies_payment_with_proper_data() + { + // Stub a successful, already verified or failed payment verification result + Toman::fakeVerification() + ->successful() + ->withOrderId('order_100') + ->withTransactionId('tid_123') + ->withReferenceId('ref_123'); + + // Toman::fakeVerification() + // ->alreadyVerified() + // ->withOrderId('order_100') + // ->withTransactionId('tid_123') + // ->withReferenceId('ref_123'); + + // Toman::fakeVerification() + // ->failed() + // ->withOrderId('order_100') + // ->withTransactionId('tid_123'); + + // Act with your app ... + + // Assert that you've correctly verified payment + Toman::assertCheckedForVerification(function ($request) { + return $request->merchantId() === 'your-idpay-api-id' + && $request->orderId() === 'order_100' + && $request->transactionId() === 'tid_123' + && $request->amount()->is(Money::Toman(50000)); + }); + } +} +``` diff --git a/docs/fa/gateways/zarinpal.md b/docs/fa/gateways/zarinpal.md new file mode 100644 index 0000000..c379d81 --- /dev/null +++ b/docs/fa/gateways/zarinpal.md @@ -0,0 +1,260 @@ +# استفاده از درگاه زرین‌پال + +تومن درگاه [Zarinpal.com](https://www.zarinpal.com) رو بر اساس نسخه 1.3 [داکیومنت رسمی‌شون](https://github.com/ZarinPal-Lab/Documentation-PaymentGateway/) ساخته. + +## شروع به کار +### تنظیمات + +درگاه زرین‌پال نیاز به تغییر این مقادیر تو فایل `.env` داره: + +| متغیر محیطی | توضیحات | +|---------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TOMAN_GATEWAY` | (**اجباری**)
باید `zarinpal` باشه. | +| `ZARINPAL_MERCHANT_ID` | (**اجباری**)
کد درگاه پرداخت که می‌تونین از پنل ارائه‌دهنده بگیرین.
نمونه: `0bcf346fc-3a79-4b36-b936-5ccbc2be0696` | +| `ZARINPAL_SANDBOX` | (اختیاری. مقدار پیش‌فرض: `false`)
اگه تنظیم شه به `true`، همه درخواست‌ها تو محیط تست این درگاه بدون پرداخت واقعی انجام می‌شه. این شرایط تو محیط توسعه لوکال به درد می‌خوره. + +نمونه: +```dotenv +... + +TOMAN_GATEWAY=zarinpal +ZARINPAL_MERCHANT_ID=0bcf346fc-3a79-4b36-b936-5ccbc2be0696 +``` + +#### واحد پولی + +واحد پولی رو می‌تونین به دو صورت معین کنین: + +**استفاده از فایل تنظیمات (کانفیگ):** + +| متد | تنظیم | یعنی | +|------------------|----------------------------|--------------| +| `amount(10000)` | `toman.currency = 'toman'` | 10,000 تومان | +| `amount(10000)` | `toman.currency = 'rial'` | 1,000 تومان | + +**صریحاً مشخص کردن:** +```php +use Evryn\LaravelToman\Money; + +...->amount(Money::Rial(10000)); +...->amount(Money::Toman(1000)); +``` + +## ⚡ درخواست پرداخت جدید + +```php +use Evryn\LaravelToman\Facades\Toman; + +// ... + +$request = Toman::amount(1000) + // ->description('Subscribing to Plan A') + // ->callback(route('payment.callback')) + // ->mobile('09350000000') + // ->email('amirreza@example.com') + ->request(); + +if ($request->successful()) { + $transactionId = $request->transactionId(); + // Store created transaction details for verification + + return $request->pay(); // Redirect to payment URL +} + +if ($request->failed()) { + // Handle transaction request failure; Probably showing proper error to user. +} +``` + +متدهای قابل استفاده برای ایجاد درخواست پرداخت جدید با کلاس نمایه‌ای `Toman`: + +| متد | توضیحات | +|------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `amount($amount)` | **(الزامی)** تنظیم مبلغ قابل پرداخت. | +| `callback($url)` | تنظیم یک آدرس URL کامل به عنوان Callback URL. بر کانفیگ `callback_route` اولویت داره. | +| `description($string)` | تنظیم توضیحات پرداخت. بر کانفیگ `description` اولویت داره. | +| `mobile($mobile)` | تنظیم شماره موبایل پرداخت‌کننده. | +| `email($email)` | تنظیم ایمیل پرداخت‌کننده. | +| `request()` | ارسال درخواست پرداخت جدید و بازگردوندن یک آبجکت از نوع `RequestedPayment`. | + + +استفاده از `RequestedPayment` برگردونده شده: + +|
متد
| توضیحات | +|-------------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `successful()` | درخواست پرداخت موفق بوده؛ شناسه تراکنش در دسترسه و می‌شه کاربر رو به صفحه پرداخت هدایت کرد. | +| `transactionId()` | [در صورت موفقیت] دریافت شناسه تراکنش. | +| `pay($options = [])` | [در صورت موفقیت] ریدایرکت کردن کاربر به صفحه پرداخت از کنترلر. یه آبجکت `RedirectResponse` برمی‌گردونه.
یه آپشن اختیاری هم می‌شه برای ارسال کرد که مشخص کننده درگاه بانکی خاص برای پرداخت هست: `['gateway' => 'Sep']`. برای استفاده، باید با زرین‌پال صحبت کنین. | +| `paymentUrl($options = [])` | [در صورت موفقیت] دریافت آدرس پراخت نهایی. آپشن اختیاریش هم مثل مورد بالا هست. | +| `failed()` | درخواست پرداخت شکست خورد؛ پیام‌های مناسب و Exception در دسترس هستن. | +| `messages()` | [در صورت شکست] دریافت آرایه‌ای از پیام‌های خطا | +| `message()` | [در صورت شکست] دریافت اولین پیام خطا. | +| `throw()` | [در صورت شکست] پرت کردن Exception متناسب با خطا. | + + + +## ⚡ تایید پرداخت + +مکانیزم تایید پرداخت باید در مسیر مربوط به Callback ارسال شده پیاده شود. این کنترلر رو در نظر بگیرین: + +```php +transactionId() to match the payment record stored + // in your persistence database and get expected amount, which is required + // for verification. + + $payment = $request->amount(1000)->verify(); + + if ($payment->successful()) { + // Store the successful transaction details + $referenceId = $payment->referenceId(); + } + + if ($payment->alreadyVerified()) { + // ... + } + + if ($payment->failed()) { + // ... + } + } +} +``` + +متدهای قابل استفاده برای تایید پرداخت با `CallbackRequest` یا کلاس نمایه‌ای `Toman`: + +| متد | توضیحات | +|------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `amount($amount)` | **(اجباری)** تنظیم مبلغی که کاربر باید پرداخت کرده باشد. | +| `transactionId($id)` | تنظیم شناسه تراکنش برای بررسی تایید پرداخت. `CallbackRequest` اینو خودش پر می‌کنه.| +| `verify()` |ارسال درخواست بررسی و تایید پرداخت. یه آبجکت `CheckedPayment` برمی‌گردونه. | + + +استفاده از `CheckedPayment` برگردونده شده: + +| متد | توضیحات | +|-------------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `transactionId()` | دریافت شناسه تراکنشی که تو درخواست ارسال شده بود. | +| `successful()` | پرداخت موفق بوده و شناسه ارجاع در دسترسه. | +| `transactionId()` | [در صورت موفقیت] دریافت شناسه ارجاع. | +| `alreadyVerified()` | پرداخت قبلاً یه بار بررسی و تایید شده بود. شناسه ارجاع همچنان در دسترسه. | +| `failed()` | پرداخت شکست خورده؛ پیام‌های مناسب و Exception در دسترس هستن. | +| `messages()` | [در صورت شکست] دریافت آرایه‌ای از پیام‌های خطا | +| `message()` | [در صورت شکست] دریافت اولین پیام خطا. | +| `throw()` | [در صورت شکست] پرت کردن Exception متناسب با خطا. | + +
+ +## بیشتر + +### تایید پرداخت به صورت دستی +اگه نیاز داشتین بدون استفاده از `CallbackRequest` تایید یه پرداخت رو بررسی کنین، می‌تونین از `Toman` استفاده کنین: + +```php +use Evryn\LaravelToman\Facades\Toman; + +// ... + +$payment = Toman::transactionId('A00001234') + ->amount(1000) + ->verify(); + +if ($payment->successful()) { + // Store the successful transaction details + $referenceId = $payment->referenceId(); +} + +if ($payment->alreadyVerified()) { + // ... +} + +if ($payment->failed()) { + // ... +} +``` + +### تست کردن درگاه IDPay +اگه که برای نرم‌افزارتون تست سوئیت خودکار می‌نویسین و می‌خواین ببینین که با پکیج به درستی تعامل داره یا نه، ادامه بدین. + +#### 🧪 تست درخواست پرداخت + +از `Toman::fakeRequest()` استفاده کنین تا یه نتیجه درخواست ایجاد پرداخت رو شبیه‌سازی کنین و بعد محتوای درخواست رو با `Toman::assertRequested()` مورد بررسی قرار بدین. + +```php +use Evryn\LaravelToman\Facades\Toman; +use Evryn\LaravelToman\Money; + +final class PaymentTest extends TestCase +{ + /** @test */ + public function requests_new_payment_with_proper_data() + { + // Stub a successful or failed payment request result + Toman::fakeRequest()->successful()->withTransactionId('A123'); + + // Toman::fakeRequest()->failed(); + + // Act with your app ... + + // Assert that you've correctly requested payment + Toman::assertRequested(function ($request) { + return $request->merchantId() === 'your-merchant-id' + && $request->callback() === route('callback-route') + && $request->amount()->is(Money::Toman(50000)); + }); + } +} +``` + +#### 🧪 تست بررسی و تایید پرداخت + +از `Toman::fakeVerification()` استفاده کنین تا یه نتیجه درخواست بررسی و تایید رو شبیه‌سازی کنین و بعد محتوای درخواست رو با `Toman::assertCheckedForVerification()` مورد بررسی قرار بدین. + +```php +use Evryn\LaravelToman\Facades\Toman; +use Evryn\LaravelToman\Money; + +final class PaymentTest extends TestCase +{ + /** @test */ + public function verifies_payment_with_proper_data() + { + // Stub a successful, already verified or failed payment verification result + Toman::fakeVerification() + ->successful() + ->withTransactionId('A123') + ->withReferenceId('R123'); + + // Toman::fakeVerification() + // ->alreadyVerified() + // ->withTransactionId('A123') + // ->withReferenceId('R123'); + + // Toman::fakeVerification() + // ->failed() + // ->withTransactionId('A123'); + + // Act with your app ... + + // Assert that you've correctly verified payment + Toman::assertCheckedForVerification(function ($request) { + return $request->merchantId() === 'your-merchant-id' + && $request->transactionId() === 'A123' + && $request->amount()->is(Money::Toman(50000)); + }); + } +} +``` diff --git a/docs/fa/getting-started.md b/docs/fa/getting-started.md new file mode 100644 index 0000000..d288eea --- /dev/null +++ b/docs/fa/getting-started.md @@ -0,0 +1,43 @@ +> اگه نیاز شد، توی [evryn/laravel-toman-example](https://github.com/evryn/laravel-toman-example) می‌تونین یه نمونه پروژه کامل با این پکیج رو ببینین. + +## پیش‌نیازها + +| پکیج | فریم‌ورک Laravel | PHP | وضعیت | +| ------------- |:-------------:|:-----:| ---:| +| ‎2.x | ‎8.x, ‎7.x | ‎>= 7.3 | فعال 🚀 | +| ‎1.x | ‎6.x, ‎5.8.x | ‎>= 7.2 | | + +## نصب + +پکیج رو با Composer نصب کنین: +```bash +composer require evryn/laravel-toman +``` + +## تنظیمات +یه سری تنظیمات در نظر گرفتیم که باعث می‌شه کدهای کم‌تری استفاده کنین. + +با دستور زیر، فایل کانفیگ پکیج رو منتشر کنین: +```bash +php artisan vendor:publish --provider=Evryn\LaravelToman\LaravelTomanServiceProvider --tag=config +``` + +حالا می‌تونین تو فایل `config/toman.php` این تنظیمات رو انجام بدین. + +## شخصی‌سازی متن‌ها (اختیاری) +درگاه‌ها ممکنه پاسخ رو در وضعیت‌های مختلفی برگردونن که هر کدوم معنی خاصی داره. تومن این متن‌ها رو هم نوشته تا کارتون راحت‌تر باشه. + +با دستور زیر، فایل مربوط به متن‌ها رو منتشر کنین: +```bash +php artisan vendor:publish --provider=Evryn\LaravelToman\LaravelTomanServiceProvider --tag=lang +``` + +فایل‌ها تو مسیر ‎`/resource/lang/vendor/toman` قابل ویرایش هستن. + + +## قدم بعدی +یکی از درگاه‌ها رو راه بندازین: + * [💳 زرین‌پال](fa/gateways/zarinpal.md) + * [💳 آی‌دی پِی](fa/gateways/idpay.md) + + diff --git a/docs/fa/support.md b/docs/fa/support.md new file mode 100644 index 0000000..a909893 --- /dev/null +++ b/docs/fa/support.md @@ -0,0 +1,40 @@ +# حمایت از ما + +این پکیج رو دوست دارین؟ ❤ـتون رو لطفاً بهمون نشون بدین: + +## کمک مالی + +یه حالی بهمون بدین :) واحد پولی‌تون رو انتخاب کنین: + * [€, £](https://dashboard.yekpay.com/en/user/AmirrezaNasiri) + * [تومان](https://zarinp.al/@amirrezan) + + +## ارسال کارت پستال + +کارت پستال مربوط به خودتون یا شرکت‌تون رو بهمون ارسال کنین تا بسی کیف کنیم :) بعداً نشون‌شون هم خواهیم داد. بهم یه پیام بفرستین تا زودی پاسخ بدم: `nasiri.amirreza.96@gmail.com` یا [@Amirreza_Nasiri](https://twitter.com/amirreza_nasiri). + + +## حمایت تو گیت‌هاب + +با فالو کردن ما، ستاره دادن به این پروژه و چنگال زدن بهش، کمک کنین تا پکیج بیشتر دیده بشه. بیشتر از 11 ثانیه طول نمی‌کشه. کافیه برین به صفحه [evryn/laravel-toman](https://github.com/evryn/laravel-toman) و دکمه‌های زیر رو توش پیدا کنین: + +
+Follow +Star +Fork +
+ +## مشارکت + +آشنا به لاراول و توسعه پکیج هستین؟ عالی می‌شه اگه بتونین تو توسعه تومن بهمون کمک کنین. واسه این کار، [Contribution Guide](../CONTRIBUTING.md) رو بخونین. + +## به‌اشتراک بذارین + +اگه کسی رو می‌شناسین که این پکیج کارهاش رو یکم راحت‌تر می‌کنه، لطفاً لینک این داکیومنت رو باهاش به‌اشتراک بذارین یا روی لینک‌های زیر کلیک کنین: + +* [به‌اشتراک‌گذاری در توئیتر](https://twitter.com/share?url=https://github.com/evryn/laravel-toman) +* [به‌اشتراک‌گذاری در تلگرام](https://t.me/share/url?url=https://github.com/evryn/laravel-toman) + +## هیچ کدومش قابل انجام نیست؟ + + اصلاً اشکال نـــــــــداره! کافیه **امروز** لبخند رو لب‌های یکی بیارین 😊 diff --git a/docs/gateways/idpay.md b/docs/gateways/idpay.md new file mode 100644 index 0000000..247250c --- /dev/null +++ b/docs/gateways/idpay.md @@ -0,0 +1,267 @@ +# Using IDPay Gateway + +Implementation of [IDPay.ir](https://idpay.ir) gateway is based on version 1.1 of their [official document](https://idpay.ir/web-service/v1.1/). + +## Getting Started +### Setup + +IDPay gateway requires the following variables in `.env` file to work: + +| Environment Variable | Description | +|---------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TOMAN_GATEWAY` | (**Required**)
Must equal `idpay` in order to use this gateway provider. | +| `IDPAY_API_KEY` | (**Required**)
Your gateway's API Key which can be gotten from your panel.
Example: `0bcf346fc-3a79-4b36-b936-5ccbc2be0696` | +| `IDPAY_SANDBOX` | (Optional. Default: `false`)
Set it to `true` in your development environment to make test calls in a simulated environment provided by the gateway without real payments. + +Example: +```dotenv +... + +TOMAN_GATEWAY=idpay +IDPAY_API_KEY=0bcf346fc-3a79-4b36-b936-5ccbc2be0696 +``` + +### Currency + +You can specify your intended currency in a few ways: + +**Using config:** + +| Method | Config | Means | +|------------------|----------------------------|--------------| +| `amount(10000)` | `toman.currency = 'toman'` | 10,000 Toman | +| `amount(10000)` | `toman.currency = 'rial'` | 1,000 Toman | + +**Specifying explicitly:** +```php +use Evryn\LaravelToman\Money; + +...->amount(Money::Rial(10000)); +...->amount(Money::Toman(1000)); +``` + +## ⚡ Request New Payment + +```php +use Evryn\LaravelToman\Facades\Toman; + +// ... + +$request = Toman::orderId('order_1500') + ->amount(15000) + // ->description('Subscribing to Plan A') + // ->callback(route('payment.callback')) + // ->mobile('09350000000') + // ->email('amirreza@example.com') + // ->name('Amirreza Nasiri') + ->request(); + +if ($request->successful()) { + $transactionId = $request->transactionId(); + // Store created transaction details for verification + + return $request->pay(); // Redirect to payment URL +} + +if ($request->failed()) { + // Handle transaction request failure; Probably showing proper error to user. +} +``` + +For requesting payment using `Toman` facade: + +| Method | Description | +|------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `amount($amount)` | **(Required)** Set amount for payment. | +| `orderId($orderId)` | **(Required)** Set order ID for payment. Order ID is a unique your-side generated string that will be used for verification. | +| `callback($url)` | Set an absolute callback URL. Overrides `callback_route` config. | +| `description($string)` | Set description. Overrides `description` config. | +| `mobile($mobile)` | Set mobile. | +| `email($email)` | Set email. | +| `name($name)` | Set payer name. | +| `request()` | Request payment and return `RequestedPayment` object | + + +Using returned `RequestedPayment`: + +|
Method
| Description | +|-------------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `successful()` | Payment request was successful, its transaction ID is available and can be redirected for payment. | +| `transactionId()` | [On Success] Get transaction ID. | +| `pay()` | [On Success] Redirect to payment URL from your controller. Returns a `RedirectResponse` object. | +| `paymentUrl()` | [On Success] Get payment URL. | +| `failed()` | Payment request was failed and proper messages and exception are available. | +| `messages()` | [On Failure] Get list of error messages. | +| `message()` | [On Failure] Get first error message. | +| `throw()` | [On Failure] Throw exception related to the failure. | + + + +## ⚡ Verify Payment + +Verification must be implemented in the callback route. Consider the following controller method: + +```php +transactionId() and $request->orderId() to match the + // non-paid payment. Take care of Double Spending. + + $payment = $request->verify(); + + if ($payment->successful()) { + // Store the successful transaction details + $referenceId = $payment->referenceId(); + } + + if ($payment->alreadyVerified()) { + // ... + } + + if ($payment->failed()) { + // ... + } + } +} +``` + +For requesting payment using `CallbackRequest` or `Toman` facade: + +| Method | Description | +|------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `orderId($orderId)` | Set the order ID you've generated when requesting the payment. `CallbackRequest` sets it automatically. | +| `transactionId($id)` | Set transaction ID to verify. `CallbackRequest` sets it automatically. | +| `verify()` | Verify payment and return `CheckedPayment` object | + + +Using returned `CheckedPayment`: + +| Method | Description | +|-------------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `orderId()` | Get order ID. | +| `transactionId()` | Get transaction ID. | +| `successful()` | Payment is newly verified and its reference ID is available. | +| `transactionId()` | [On Success] Get reference ID. | +| `alreadyVerified()` | Payment was once verified before. Reference ID is available too. | +| `failed()` | Payment was failed and proper messages and exception are available. | +| `messages()` | [On Failure] Get list of error messages. | +| `message()` | [On Failure] Get first error message. | +| `throw()` | [On Failure] Throw exception related to the failure. | + +
+ +## More + +### Manual Payment Verification +If you want to verify a payment manually (instead of using intended `CallbackRequest`), you just need to use `Toman` facade: +```php +use Evryn\LaravelToman\Facades\Toman; + +// ... + +$payment = Toman::transactionId('tid_123') + ->orderId('order_1000') + ->verify(); + +if ($payment->successful()) { + // Store the successful transaction details + $referenceId = $payment->referenceId(); +} + +if ($payment->alreadyVerified()) { + // ... +} + +if ($payment->failed()) { + // ... +} +``` + +### Testing IDPay Gateway +If you're making automated tests for your application and want to see if you're interacting with Laravel Toman properly, go on. + +#### 🧪 Test Payment Request + +Use `Toman::fakeRequest()` to stub request result and assert expected request data with `Toman::assertRequested()` method by a truth test. + +```php +use Evryn\LaravelToman\Facades\Toman; +use Evryn\LaravelToman\Money; + +final class PaymentTest extends TestCase +{ + /** @test */ + public function requests_new_payment_with_proper_data() + { + // Stub a successful or failed payment request result + Toman::fakeRequest()->successful()->withTransactionId('tid_123'); + + // Toman::fakeRequest()->failed(); + + // Act with your app ... + + // Assert that you've correctly requested payment + Toman::assertRequested(function ($request) { + return $request->merchantId() === 'your-idpay-api-key' + && $request->callback() === route('callback-route') + && $request->amount()->is(Money::Toman(50000)); + }); + } +} +``` + +#### 🧪 Test Payment Verification + +Use `Toman::fakeVerification()` to stub verification result and assert its expected data with `Toman::assertCheckedForVerification()` method by a truth test. + +```php +use Evryn\LaravelToman\Facades\Toman; +use Evryn\LaravelToman\Money; + +final class PaymentTest extends TestCase +{ + /** @test */ + public function verifies_payment_with_proper_data() + { + // Stub a successful, already verified or failed payment verification result + Toman::fakeVerification() + ->successful() + ->withOrderId('order_100') + ->withTransactionId('tid_123') + ->withReferenceId('ref_123'); + + // Toman::fakeVerification() + // ->alreadyVerified() + // ->withOrderId('order_100') + // ->withTransactionId('tid_123') + // ->withReferenceId('ref_123'); + + // Toman::fakeVerification() + // ->failed() + // ->withOrderId('order_100') + // ->withTransactionId('tid_123'); + + // Act with your app ... + + // Assert that you've correctly verified payment + Toman::assertCheckedForVerification(function ($request) { + return $request->merchantId() === 'your-idpay-api-id' + && $request->orderId() === 'order_100' + && $request->transactionId() === 'tid_123' + && $request->amount()->is(Money::Toman(50000)); + }); + } +} +``` diff --git a/docs/gateways/zarinpal.md b/docs/gateways/zarinpal.md index 3091097..3fbeee7 100644 --- a/docs/gateways/zarinpal.md +++ b/docs/gateways/zarinpal.md @@ -1,16 +1,17 @@ -# Zarinpal Gateway +# Using Zarinpal Gateway -[Zarinpal.com](https://www.zarinpal.com) gateway was added in version 1.0 and implementation of it is based on version 1.3 of their [official document](https://github.com/ZarinPal-Lab/Documentation-PaymentGateway/). +Implementation of [Zarinpal.com](https://www.zarinpal.com) gateway is based on version 1.3 of their [official document](https://github.com/ZarinPal-Lab/Documentation-PaymentGateway/). -## Config +## Getting Started +### Setup -Zarinpal gateway requires following variables in `.env` file to work: +Zarinpal gateway requires the following variables in `.env` file to work: | Environment Variable | Description | |---------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| TOMAN_GATEWAY | (**Required**)
Must equal `zarinpal` in order to use this gateway provider. | -| ZARINPAL_MERCHANT_ID | (**Required**)
Your gateway's merchant ID which can be gotten from your Zarinpal panel.
Example: 0bcf346fc-3a79-4b36-b936-5ccbc2be0696 | -| ZARINPAL_SANDBOX | (Optional. Default: false)
Set it to `true` to make test calls in a simulated environment provided by Zarinpal without actual payments.
Delete the key or set it to `false` to make calls in production environment and receive actual payments. | +| `TOMAN_GATEWAY` | (**Required**)
Must equal `zarinpal` in order to use this gateway provider. | +| `ZARINPAL_MERCHANT_ID` | (**Required**)
Your gateway's merchant ID which can be gotten from your panel.
Example: `0bcf346fc-3a79-4b36-b936-5ccbc2be0696` | +| `ZARINPAL_SANDBOX` | (Optional. Default: `false`)
Set it to `true` in your development environment to make test calls in a simulated environment provided by the gateway without real payments. Example: ```dotenv @@ -20,76 +21,239 @@ TOMAN_GATEWAY=zarinpal ZARINPAL_MERCHANT_ID=0bcf346fc-3a79-4b36-b936-5ccbc2be0696 ``` -## Payment Request +### Currency + +You can specify your intended currency in a few ways: + +**Using config:** + +| Method | Config | Means | +|------------------|----------------------------|--------------| +| `amount(10000)` | `toman.currency = 'toman'` | 10,000 Toman | +| `amount(10000)` | `toman.currency = 'rial'` | 1,000 Toman | + +**Specifying explicitly:** +```php +use Evryn\LaravelToman\Money; + +...->amount(Money::Rial(10000)); +...->amount(Money::Toman(1000)); +``` + +## ⚡ Request New Payment -In short, you can request a new payment with the following methods and catches: ```php -use Evryn\LaravelToman\Facades\PaymentRequest; -use Evryn\LaravelToman\Gateways\Zarinpal\Status; +use Evryn\LaravelToman\Facades\Toman; // ... -try { - $requestedPayment = PaymentRequest::amount(1000) - ->description('Subscrbing to Plan A') - ->callback('https://example.com/callback') - ->mobile('09350000000') - ->email('amirreza@example.com') - ->request(); -} catch (GatewayException $gatewayException) { - if ($gatewayException->getCode() === Status::SHAPARAK_LIMITED) { - // there might be a problem with 'Amount' data. - } -} catch (InvalidConfigException $exception) { - // Woops! wrong merchant_id or sandbox option is configurated. +$request = Toman::amount(1000) + // ->description('Subscribing to Plan A') + // ->callback(route('payment.callback')) + // ->mobile('09350000000') + // ->email('amirreza@example.com') + ->request(); + +if ($request->successful()) { + $transactionId = $request->transactionId(); + // Store created transaction details for verification + + return $request->pay(); // Redirect to payment URL } -// Use $requestedPayment... +if ($request->failed()) { + // Handle transaction request failure; Probably showing proper error to user. +} ``` +For requesting payment using `Toman` facade: + | Method | Description | |------------- |--------------------------------------------------------------------------------------------------------------------------------- | -| callback | Sets `CallbackURL` data and overrides `callback_route` config. | -| description | Sets `Description` data and overrides `description` config. | -| mobile | Sets `Mobile` data. | -| email | Sets `Email` data. | -| amount | Sets `Amount` data. | -| request | Calls `PaymentRequest` Zarinpal endpoint and returns a `RequestedPayment` object with available transaction ID and payment URL.
Might throw `GatewayException` if the request was rejected by Zarinpal. `GatewayException` is filled with user-friendly message that can be translated (see [Translations](#translations) section) and status code constants in `Evryn\LaravelToman\Gateways\Zarinpal\Status`.
Might throw an `InvalidConfigException` if gateway-specific configs are not set correctly (see [Config](#config) section above). | +| `amount($amount)` | **(Required)** Set amount for payment. | +| `callback($url)` | Set an absolute callback URL. Overrides `callback_route` config. | +| `description($string)` | Set description. Overrides `description` config. | +| `mobile($mobile)` | Set mobile. | +| `email($email)` | Set email. | +| `request()` | Request payment and return `RequestedPayment` object | + + +Using returned `RequestedPayment`: + +|
Method
| Description | +|-------------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `successful()` | Payment request was successful, its transaction ID is available and can be redirected for payment. | +| `transactionId()` | [On Success] Get transaction ID. | +| `pay($options = [])` | [On Success] Redirect to payment URL from your controller. Returns a `RedirectResponse` object.
An option can be optionally passed in to explicitly select final gateway: `['gateway' => 'Sep']` . You can contact ZarinPal to make them available. | +| `paymentUrl($options = [])` | [On Success] Get payment URL. `$options` is optional and same as above. | +| `failed()` | Payment request was failed and proper messages and exception are available. | +| `messages()` | [On Failure] Get list of error messages. | +| `message()` | [On Failure] Get first error message. | +| `throw()` | [On Failure] Throw exception related to the failure. | + -## Payment Verification +## ⚡ Verify Payment + +Verification must be implemented in the callback route. Consider the following controller method: -In short, you can verify a payment callback with the following methods and catches: ```php -use Evryn\LaravelToman\Facades\PaymentVerification; -use Evryn\LaravelToman\Gateways\Zarinpal\Status; +verify(request()); // or $request in your Controller -} catch (GatewayException $gatewayException) { - if ($gatewayException->getCode() === Status::NOT_PAID) { - // the payment has been cancelled - } elseif ($gatewayException->getCode() === Status::ALREADY_VERIFIED) { - // the payment has already been verified before +class PaymentController extends Controller +{ + /** + * Handle payment callback + */ + public function callback(CallbackRequest $request) + { + // Use $request->transactionId() to match the payment record stored + // in your persistence database and get expected amount, which is required + // for verification. + + $payment = $request->amount(1000)->verify(); + + if ($payment->successful()) { + // Store the successful transaction details + $referenceId = $payment->referenceId(); + } + + if ($payment->alreadyVerified()) { + // ... + } + + if ($payment->failed()) { + // ... + } } -} catch (InvalidConfigException $exception) { - // Woops! wrong merchant_id or sandbox option is configurated. } - -// Use $verifiedPayment... ``` +For requesting payment using `CallbackRequest` or `Toman` facade: + | Method | Description | |------------- |--------------------------------------------------------------------------------------------------------------------------------- | -| amount | Sets `Amount` data. It should equal to the amount that payment request was created with. | -| verify | Calls `PaymentVerification` Zarinpal endpoint with callback queries gotten from request and returns a `VerifiedPayment` object with available reference ID.
Might throw a `GatewayException` if the request was rejected by Zarinpal. `GatewayException` is filled with user-friendly message that can be translated (see [Translations](#translations) section) and status code constants in `Evryn\LaravelToman\Gateways\Zarinpal\Status`.
Might throw an `InvalidConfigException` if gateway-specific configs are not set correctly (see [Config](#config) section above) | - +| `amount($amount)` | **(Required)** Set amount that is expected to be paid. | +| `transactionId($id)` | Set transaction ID to verify. `CallbackRequest` sets it automatically. | +| `verify()` | Verify payment and return `CheckedPayment` object | + + +Using returned `CheckedPayment`: + +| Method | Description | +|-------------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| `transactionId()` | Get transaction ID. | +| `successful()` | Payment is newly verified and its reference ID is available. | +| `transactionId()` | [On Success] Get reference ID. | +| `alreadyVerified()` | Payment was once verified before. Reference ID is available too. | +| `failed()` | Payment was failed and proper messages and exception are available. | +| `messages()` | [On Failure] Get list of error messages. | +| `message()` | [On Failure] Get first error message. | +| `throw()` | [On Failure] Throw exception related to the failure. | -## Translations +
-Currently, two `fa` (Persian) and `en` (English) languages are added to display payment status (in `GatewayException` especially). +## More + +### Manual Payment Verification +If you want to verify a payment manually (instead of using intended `CallbackRequest`), you just need to use `Toman` facade: +```php +use Evryn\LaravelToman\Facades\Toman; + +// ... -See [Getting Started/Translations](../translations.md) to find out how to set application locale or customize these messages. +$payment = Toman::transactionId('A00001234') + ->amount(1000) + ->verify(); + +if ($payment->successful()) { + // Store the successful transaction details + $referenceId = $payment->referenceId(); +} + +if ($payment->alreadyVerified()) { + // ... +} + +if ($payment->failed()) { + // ... +} +``` + +### Testing Zarinpal Gateway +If you're making automated tests for your application and want to see if you're interacting with Laravel Toman properly, go on. + +#### 🧪 Test Payment Request + +Use `Toman::fakeRequest()` to stub request result and assert expected request data with `Toman::assertRequested()` method by a truth test. + +```php +use Evryn\LaravelToman\Facades\Toman; +use Evryn\LaravelToman\Money; + +final class PaymentTest extends TestCase +{ + /** @test */ + public function requests_new_payment_with_proper_data() + { + // Stub a successful or failed payment request result + Toman::fakeRequest()->successful()->withTransactionId('A123'); + + // Toman::fakeRequest()->failed(); + + // Act with your app ... + + // Assert that you've correctly requested payment + Toman::assertRequested(function ($request) { + return $request->merchantId() === 'your-merchant-id' + && $request->callback() === route('callback-route') + && $request->amount()->is(Money::Toman(50000)); + }); + } +} +``` + +#### 🧪 Test Payment Verification + +Use `Toman::fakeVerification()` to stub verification result and assert its expected data with `Toman::assertCheckedForVerification()` method by a truth test. + +```php +use Evryn\LaravelToman\Facades\Toman; +use Evryn\LaravelToman\Money; + +final class PaymentTest extends TestCase +{ + /** @test */ + public function verifies_payment_with_proper_data() + { + // Stub a successful, already verified or failed payment verification result + Toman::fakeVerification() + ->successful() + ->withTransactionId('A123') + ->withReferenceId('R123'); + + // Toman::fakeVerification() + // ->alreadyVerified() + // ->withTransactionId('A123') + // ->withReferenceId('R123'); + + // Toman::fakeVerification() + // ->failed() + // ->withTransactionId('A123'); + + // Act with your app ... + + // Assert that you've correctly verified payment + Toman::assertCheckedForVerification(function ($request) { + return $request->merchantId() === 'your-merchant-id' + && $request->transactionId() === 'A123' + && $request->amount()->is(Money::Toman(50000)); + }); + } +} +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..26acdf8 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,42 @@ +> There is an example project using Laravel Toman you can find at [evryn/laravel-toman-example](https://github.com/evryn/laravel-toman-example). It contains a payment implementation and few critical tests. + +## Requirements + +| Package | Laravel Framework | PHP | Status | +| ------------- |:-------------:|:-----:| ---:| +| 2.x | 8.x, 7.x | >= 7.3 | Active 🚀 | +| 1.x | 6.x, 5.8.x | >= 7.2 | | + +## Installation + +Install the package using Composer: +```bash +composer require evryn/laravel-toman +``` + +## Configuration + +There are few configurable options to make your code cleaner. + +Use the following command to publish package config: +```bash +php artisan vendor:publish --provider=Evryn\LaravelToman\LaravelTomanServiceProvider --tag=config +``` + +Now, a config file will be available to edit at `config/toman.php`. See available options there. + +## Customizing Messages (Optional) + +Gateways requests might result in different states, and each of them is meaningful. This package also contains those messages, so you don't need to write them again. + +Use the following command to publish package translation files: +```bash +php artisan vendor:publish --provider=Evryn\LaravelToman\LaravelTomanServiceProvider --tag=lang +``` + +Now, translation files are ready to be modified in `/resource/lang/vendor/toman`. + +## Next Step +See how to use a gateway: + * [💳 Zarinpal](gateways/zarinpal.md) + * [💳 IDPay](gateways/idpay.md) diff --git a/docs/index.html b/docs/index.html index 92c2851..094768c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,22 +7,82 @@ + + - +
Opening wallet ...
+ diff --git a/docs/support.md b/docs/support.md index eb46f0e..bfe82d4 100644 --- a/docs/support.md +++ b/docs/support.md @@ -4,27 +4,28 @@ Do you like this project? Here are some ways to show us your ❤: ### Donation -Donations are very welcome. You can send us some money using one of the following links: - * [YekPay Personal Gateway (€, £)](https://dashboard.yekpay.com/en/user/AmirrezaNasiri) - * [Zarinpal Personal Gateway (تومان)](https://zarinp.al/@amirrezan) - -### Send us a Postcard +Donations are very welcome. Select your currency: + * [€, £](https://dashboard.yekpay.com/en/user/AmirrezaNasiri) + * [تومان](https://zarinp.al/@amirrezan) + + +### Send Us a Postcard -We appreciate to receive a physical postcard from you or your company. We'll soon display all of them. Just leave me a message and I'll reply soon: `nasiri.amirreza.96@gmail.com` or [@Amirreza_Nasiri](https://twitter.com/amirreza_nasiri). +We appreciate to receive a postcard from you or your company. We'll soon display all of them. Just leave me a message and I'll reply soon: `nasiri.amirreza.96@gmail.com` or [@Amirreza_Nasiri](https://twitter.com/amirreza_nasiri). ### Follow, Star and Fork By following us, giving a star and forking this repository, you'll make this package more visible to people. It'll take ~11s, just hit these buttons on [the repository](https://github.com/evryn/laravel-toman): -Follow @Evryn +Follow Star Fork ### Contribute -Knowing Laravel and package development? It would be great if you could help us extend this project. See [Contribution Guide](contributing.md). +Knowing about Laravel and package development? It would be great if you could help us extend this project. See [Contribution Guide](../CONTRIBUTING.md). ### Share it @@ -32,8 +33,8 @@ Let people know about this package by simply sharing current URL or clicking her * [Share on Twitter](https://twitter.com/share?url=https://github.com/evryn/laravel-toman) * [Share on Telegram](https://t.me/share/url?url=https://github.com/evryn/laravel-toman) - - + + ### Not a suitable one yet? -Don't worry at all! You can help us by making someone very very happy **today** 😊. We believe in Butterfly Effect. +Don't worry at all! You can help us by making someone happy **today** 😊 diff --git a/docs/todo.md b/docs/todo.md deleted file mode 100644 index e2b9a4d..0000000 --- a/docs/todo.md +++ /dev/null @@ -1,3 +0,0 @@ - * Add a callback request form validator with access to payment transaction id - * Add a method to continue unexpired transaction - * Add a feature to request gateway to expire specific transaction(s) diff --git a/docs/v1/_media/icon.png b/docs/v1/_media/icon.png new file mode 100644 index 0000000..6165180 Binary files /dev/null and b/docs/v1/_media/icon.png differ diff --git a/docs/v1/_media/logo.png b/docs/v1/_media/logo.png new file mode 100644 index 0000000..9dad305 Binary files /dev/null and b/docs/v1/_media/logo.png differ diff --git a/docs/_media/payment-request-canvas.png b/docs/v1/_media/payment-request-canvas.png similarity index 100% rename from docs/_media/payment-request-canvas.png rename to docs/v1/_media/payment-request-canvas.png diff --git a/docs/_media/payment-verification-canvas.png b/docs/v1/_media/payment-verification-canvas.png similarity index 100% rename from docs/_media/payment-verification-canvas.png rename to docs/v1/_media/payment-verification-canvas.png diff --git a/docs/v1/_sidebar.md b/docs/v1/_sidebar.md new file mode 100644 index 0000000..726a00f --- /dev/null +++ b/docs/v1/_sidebar.md @@ -0,0 +1,18 @@ +* [Introduction](introduction.md) + +* Getting started + + * [Quick Start](quickstart.md) + * [Configuration](configuration.md) + * [Translations](translations.md) + +* Available Gateways + + * [Zarinpal](gateways/zarinpal.md) + +* [Testing](testing.md) + +* Contributing +* Changelog +* Support 💜 + diff --git a/docs/configuration.md b/docs/v1/configuration.md similarity index 100% rename from docs/configuration.md rename to docs/v1/configuration.md diff --git a/docs/v1/gateways/zarinpal.md b/docs/v1/gateways/zarinpal.md new file mode 100644 index 0000000..3091097 --- /dev/null +++ b/docs/v1/gateways/zarinpal.md @@ -0,0 +1,95 @@ +# Zarinpal Gateway + +[Zarinpal.com](https://www.zarinpal.com) gateway was added in version 1.0 and implementation of it is based on version 1.3 of their [official document](https://github.com/ZarinPal-Lab/Documentation-PaymentGateway/). + +## Config + +Zarinpal gateway requires following variables in `.env` file to work: + +| Environment Variable | Description | +|---------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| TOMAN_GATEWAY | (**Required**)
Must equal `zarinpal` in order to use this gateway provider. | +| ZARINPAL_MERCHANT_ID | (**Required**)
Your gateway's merchant ID which can be gotten from your Zarinpal panel.
Example: 0bcf346fc-3a79-4b36-b936-5ccbc2be0696 | +| ZARINPAL_SANDBOX | (Optional. Default: false)
Set it to `true` to make test calls in a simulated environment provided by Zarinpal without actual payments.
Delete the key or set it to `false` to make calls in production environment and receive actual payments. | + +Example: +```dotenv +... + +TOMAN_GATEWAY=zarinpal +ZARINPAL_MERCHANT_ID=0bcf346fc-3a79-4b36-b936-5ccbc2be0696 +``` + +## Payment Request + +In short, you can request a new payment with the following methods and catches: +```php +use Evryn\LaravelToman\Facades\PaymentRequest; +use Evryn\LaravelToman\Gateways\Zarinpal\Status; + +// ... + +try { + $requestedPayment = PaymentRequest::amount(1000) + ->description('Subscrbing to Plan A') + ->callback('https://example.com/callback') + ->mobile('09350000000') + ->email('amirreza@example.com') + ->request(); +} catch (GatewayException $gatewayException) { + if ($gatewayException->getCode() === Status::SHAPARAK_LIMITED) { + // there might be a problem with 'Amount' data. + } +} catch (InvalidConfigException $exception) { + // Woops! wrong merchant_id or sandbox option is configurated. +} + +// Use $requestedPayment... +``` + +| Method | Description | +|------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| callback | Sets `CallbackURL` data and overrides `callback_route` config. | +| description | Sets `Description` data and overrides `description` config. | +| mobile | Sets `Mobile` data. | +| email | Sets `Email` data. | +| amount | Sets `Amount` data. | +| request | Calls `PaymentRequest` Zarinpal endpoint and returns a `RequestedPayment` object with available transaction ID and payment URL.
Might throw `GatewayException` if the request was rejected by Zarinpal. `GatewayException` is filled with user-friendly message that can be translated (see [Translations](#translations) section) and status code constants in `Evryn\LaravelToman\Gateways\Zarinpal\Status`.
Might throw an `InvalidConfigException` if gateway-specific configs are not set correctly (see [Config](#config) section above). | + + +## Payment Verification + +In short, you can verify a payment callback with the following methods and catches: +```php +use Evryn\LaravelToman\Facades\PaymentVerification; +use Evryn\LaravelToman\Gateways\Zarinpal\Status; + +// ... + +try { + $verifiedPayment = PaymentVerification::amount(1000) + ->verify(request()); // or $request in your Controller +} catch (GatewayException $gatewayException) { + if ($gatewayException->getCode() === Status::NOT_PAID) { + // the payment has been cancelled + } elseif ($gatewayException->getCode() === Status::ALREADY_VERIFIED) { + // the payment has already been verified before + } +} catch (InvalidConfigException $exception) { + // Woops! wrong merchant_id or sandbox option is configurated. +} + +// Use $verifiedPayment... +``` + +| Method | Description | +|------------- |--------------------------------------------------------------------------------------------------------------------------------- | +| amount | Sets `Amount` data. It should equal to the amount that payment request was created with. | +| verify | Calls `PaymentVerification` Zarinpal endpoint with callback queries gotten from request and returns a `VerifiedPayment` object with available reference ID.
Might throw a `GatewayException` if the request was rejected by Zarinpal. `GatewayException` is filled with user-friendly message that can be translated (see [Translations](#translations) section) and status code constants in `Evryn\LaravelToman\Gateways\Zarinpal\Status`.
Might throw an `InvalidConfigException` if gateway-specific configs are not set correctly (see [Config](#config) section above) | + + +## Translations + +Currently, two `fa` (Persian) and `en` (English) languages are added to display payment status (in `GatewayException` especially). + +See [Getting Started/Translations](../translations.md) to find out how to set application locale or customize these messages. diff --git a/docs/v1/index.html b/docs/v1/index.html new file mode 100644 index 0000000..4c89b6e --- /dev/null +++ b/docs/v1/index.html @@ -0,0 +1,62 @@ + + + + Laravel Toman + + + + + + + + + +
+ + + Version 1.x is not maintained anymore. Read documents of version 2.x. + + +
+
Opening wallet ...
+ + + + + + + diff --git a/docs/introduction.md b/docs/v1/introduction.md similarity index 89% rename from docs/introduction.md rename to docs/v1/introduction.md index c0df383..c49018d 100644 --- a/docs/introduction.md +++ b/docs/v1/introduction.md @@ -4,8 +4,8 @@ Toman is a Laravel package which makes working with popular payment gateways muc There are dozens of gateway handlers; Here is why you may choose Laravel Toman: ## Heavily Tested - - Build Status + + Build Status Code Coverage diff --git a/docs/quickstart.md b/docs/v1/quickstart.md similarity index 78% rename from docs/quickstart.md rename to docs/v1/quickstart.md index 3fc8603..6734074 100644 --- a/docs/quickstart.md +++ b/docs/v1/quickstart.md @@ -1,18 +1,15 @@ -> There is an example project using Laravel Toman you can find at [evryn/laravel-toman-example](https://github.com/evryn/laravel-toman-example). It contains payment code and few key tests. - # Requirements -Every released build is tested against combination of following config: - * PHP: 7.2+ - * Laravel: 5.8+ - - If you're not using this config, you should really take a look at lifetime support of Laravel and PHP. +| Package | Laravel Framework | PHP | Status | +| ------------- |:-------------:|:-----:| ---:| +| v2 | 8.x, 7.x | >= 7.3 | Active 🚀 | +| v1 | 6.x, 5.8.x | >= 7.2 | | # Installation Install package using composer: ```bash -composer require evryn/laravel-toman +composer require "evryn/laravel-toman":"^1.1" ``` # Setup diff --git a/docs/testing.md b/docs/v1/testing.md similarity index 100% rename from docs/testing.md rename to docs/v1/testing.md diff --git a/docs/translations.md b/docs/v1/translations.md similarity index 100% rename from docs/translations.md rename to docs/v1/translations.md diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 518e1d1..f83f8af 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,7 +10,7 @@ processIsolation="false" stopOnFailure="false"> - + ./tests/ diff --git a/src/CallbackRequest.php b/src/CallbackRequest.php new file mode 100644 index 0000000..17c534e --- /dev/null +++ b/src/CallbackRequest.php @@ -0,0 +1,30 @@ +factory = $factory; + } + + public function validateResolved() + { + return $this->factory = $this->factory->inspectCallbackRequest(); + } + + public function __call($name, $arguments) + { + return $this->factory->{$name}(...$arguments); + } +} diff --git a/src/Clients/GuzzleClient.php b/src/Clients/GuzzleClient.php deleted file mode 100644 index b6d5f9d..0000000 --- a/src/Clients/GuzzleClient.php +++ /dev/null @@ -1,7 +0,0 @@ -fakeVerification = $fakeVerification; + } + + final public function validateResolved() + { + } + + public function validateCallback() + { + parent::validateResolved(); + } +} diff --git a/src/Concerns/CheckedPayment.php b/src/Concerns/CheckedPayment.php new file mode 100644 index 0000000..ef3b5f9 --- /dev/null +++ b/src/Concerns/CheckedPayment.php @@ -0,0 +1,40 @@ +failed()) { + $this->throw(); + } + + return $this->referenceId; + } + + public function transactionId(): string + { + return $this->transactionId; + } + + public function __call($name, $arguments) + { + throw new \BadMethodCallException(sprintf( + 'This gateway does not support `%s` method.', $name + )); + } +} diff --git a/src/Concerns/InteractsWithResponse.php b/src/Concerns/InteractsWithResponse.php new file mode 100644 index 0000000..73240a6 --- /dev/null +++ b/src/Concerns/InteractsWithResponse.php @@ -0,0 +1,52 @@ +failed()) { + throw $this->exception; + } + } + + public function status() + { + return $this->failed() ? $this->exception->getCode() : null; + } + + public function message(): ?string + { + return Arr::first( + Arr::flatten( + $this->messages() + ) + ); + } + + public function messages(): array + { + if ($this->messages) { + return $this->messages; + } + + if ($this->failed()) { + return [$this->exception->getMessage()]; + } + + return []; + } +} diff --git a/src/Concerns/RequestedPayment.php b/src/Concerns/RequestedPayment.php new file mode 100644 index 0000000..0f41551 --- /dev/null +++ b/src/Concerns/RequestedPayment.php @@ -0,0 +1,33 @@ +failed()) { + $this->throw(); + } + + return $this->transactionId; + } + + /** + * Redirect user to payment gateway to complete it. + * @param array $options + * @return RedirectResponse + */ + public function pay(array $options = []): RedirectResponse + { + return redirect()->to($this->paymentUrl($options)); + } +} diff --git a/src/Contracts/PaymentRequester.php b/src/Contracts/PaymentRequester.php deleted file mode 100644 index 2346148..0000000 --- a/src/Contracts/PaymentRequester.php +++ /dev/null @@ -1,50 +0,0 @@ -gateway = $gateway; + } + + public function fakeRequest() + { + $this->record(); + + return $this->fakeRequest = new FakeRequest(); + } + + public function fakeVerification() + { + $this->record(); + + return $this->fakeVerification = new FakeVerification(); + } + + /** + * Assert that a payment request is recorded matching a given truth test. + * + * @param callable $callback + * @return void + */ + public function assertRequested($callback) + { + if (! $this->recordedPendingRequest || ! $this->fakeRequest) { + PHPUnit::fail('No payment request is recorded.'); + } + + PHPUnit::assertTrue( + $this->isRecorded($callback), + 'Recorded payment request does not match the expectation.' + ); + } + + /** + * Assert that a payment verification is recorded matching a given truth test. + * + * @param callable $callback + * @return void + */ + public function assertCheckedForVerification($callback) + { + if (! $this->recordedPendingRequest || ! $this->fakeVerification) { + PHPUnit::fail('No payment verification is recorded.'); + } + + PHPUnit::assertTrue( + $this->isRecorded($callback), + 'Recorded payment verification does not match the expectation.' + ); + } + + private function record() + { + $this->recording = true; + } + + public function recordPendingRequest($pendingRequest) + { + if ($this->recording) { + $this->recordedPendingRequest = $pendingRequest; + } + } + + /** + * Determine if requested with given truth test. + * + * @param null|callable $callback + * @return bool + */ + private function isRecorded($callback = null) + { + if (empty($this->recordedPendingRequest)) { + return false; + } + + $callback = $callback ?: function () { + return true; + }; + + return $callback($this->recordedPendingRequest); + } + + public function newPendingRequest(): PendingRequest + { + return new PendingRequest($this, $this->gateway); + } + + /** + * Execute a method against a new pending request instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + if (static::hasMacro($method)) { + return $this->macroCall($method, $parameters); + } + + return tap($this->newPendingRequest(), function ($pendingRequest) { + $pendingRequest->stub($this->fakeRequest, $this->fakeVerification); + })->{$method}(...$parameters); + } +} diff --git a/src/FakeRequest.php b/src/FakeRequest.php new file mode 100644 index 0000000..f7637b2 --- /dev/null +++ b/src/FakeRequest.php @@ -0,0 +1,53 @@ +status = self::SUCCESSFUL; + + return $this; + } + + public function failed($error = 'Stubbed payment failure.', $status = 400): self + { + $this->status = self::FAILED; + + $this->exception = new GatewayException($error, $status); + + return $this; + } + + public function withTransactionId(string $transactionId) + { + $this->transactionId = $transactionId; + + return $this; + } + + public function getTransactionId() + { + return $this->transactionId; + } + + public function getException() + { + return $this->exception; + } + + public function getStatus() + { + return $this->status; + } +} diff --git a/src/FakeVerification.php b/src/FakeVerification.php new file mode 100644 index 0000000..a17fb02 --- /dev/null +++ b/src/FakeVerification.php @@ -0,0 +1,86 @@ +status = self::SUCCESSFUL; + + return $this; + } + + public function alreadyVerified(): self + { + $this->status = self::ALREADY_VERIFIED; + + return $this; + } + + public function failed($error = 'Stubbed payment failure.', $status = 400): self + { + $this->status = self::FAILED; + $this->exception = new GatewayException($error, $status); + + return $this; + } + + public function withTransactionId(string $transactionId): self + { + $this->transactionId = $transactionId; + + return $this; + } + + public function withOrderId(string $orderId): self + { + $this->orderId = $orderId; + + return $this; + } + + public function withReferenceId(string $referenceId): self + { + $this->referenceId = $referenceId; + + return $this; + } + + public function getTransactionId() + { + return $this->transactionId; + } + + public function getOrderId() + { + return $this->orderId; + } + + public function getReferenceId() + { + return $this->referenceId; + } + + public function getException() + { + return $this->exception; + } + + public function getStatus() + { + return $this->status; + } +} diff --git a/src/Gateways/BaseRequester.php b/src/Gateways/BaseRequester.php deleted file mode 100644 index 2dad61f..0000000 --- a/src/Gateways/BaseRequester.php +++ /dev/null @@ -1,46 +0,0 @@ -config = $config; - - return $this; - } - - public function getConfig($key = null) - { - return $key ? Arr::get($this->config, $key) : $this->config; - } - - public function data($key, $value = null) - { - $this->data[$key] = $value; - - return $this; - } - - public function getData($key = null) - { - return $key ? Arr::get($this->data, $key) : $this->data; - } -} diff --git a/src/Gateways/BaseVerifier.php b/src/Gateways/BaseVerifier.php deleted file mode 100644 index dcbe23b..0000000 --- a/src/Gateways/BaseVerifier.php +++ /dev/null @@ -1,43 +0,0 @@ -config = $config; - - return $this; - } - - public function getConfig($key = null) - { - return $key ? Arr::get($this->config, $key) : $this->config; - } - - public function data($key, $value = null) - { - $this->data[$key] = $value; - - return $this; - } - - public function getData($key = null) - { - return $key ? Arr::get($this->data, $key) : $this->data; - } -} diff --git a/src/Gateways/IDPay/BaseRequest.php b/src/Gateways/IDPay/BaseRequest.php new file mode 100644 index 0000000..eff7a7b --- /dev/null +++ b/src/Gateways/IDPay/BaseRequest.php @@ -0,0 +1,49 @@ +pendingRequest = $pendingRequest; + } + + /** + * Make request headers. + * + * @return array + */ + protected function makeHeaders(): array + { + $headers = [ + 'X-API-KEY' => $this->pendingRequest->merchantId(), + ]; + + if ($this->pendingRequest->getGateway()->getConfig('sandbox') === true) { + $headers['X-SANDBOX'] = true; + } + + return $headers; + } + + /** + * Make environment-aware verification endpoint URL. + * @param string $method + * @return string + */ + protected function getEndpoint(string $method): string + { + return "https://api.idpay.ir/v1.1/{$method}"; + } +} diff --git a/src/Gateways/IDPay/CallbackRequest.php b/src/Gateways/IDPay/CallbackRequest.php new file mode 100644 index 0000000..295ea79 --- /dev/null +++ b/src/Gateways/IDPay/CallbackRequest.php @@ -0,0 +1,47 @@ +fakeVerification) { + $this->merge([ + 'id' => $this->fakeVerification->getTransactionId(), + 'order_id' => $this->fakeVerification->getOrderId(), + ]); + } + } + + public function rules() + { + return [ + 'id' => 'required|string', + 'order_id' => 'required|string', + ]; + } + + public function messages() + { + return [ + 'id' => 'IDPay transaction id', + 'order_id' => 'IDPay order id', + ]; + } + + public function getTransactionId() + { + return $this->input('id'); + } + + public function getOrderId() + { + return $this->input('order_id'); + } +} diff --git a/src/Gateways/IDPay/CheckedPayment.php b/src/Gateways/IDPay/CheckedPayment.php new file mode 100644 index 0000000..688858c --- /dev/null +++ b/src/Gateways/IDPay/CheckedPayment.php @@ -0,0 +1,53 @@ +referenceId = $referenceId; + $this->exception = $exception; + $this->messages = $messages; + $this->status = $status; + $this->transactionId = $transactionId; + $this->orderId = $orderId; + } + + public function status() + { + return $this->status; + } + + public function orderId(): string + { + return $this->orderId; + } + + public function successful(): bool + { + return (int) $this->status === Status::SUCCESSFUL; + } + + public function alreadyVerified(): bool + { + return (int) $this->status === Status::ALREADY_VERIFIED; + } + + public function failed(): bool + { + return (bool) $this->exception; + } +} diff --git a/src/Gateways/IDPay/Gateway.php b/src/Gateways/IDPay/Gateway.php new file mode 100644 index 0000000..e93b6b4 --- /dev/null +++ b/src/Gateways/IDPay/Gateway.php @@ -0,0 +1,97 @@ +config = $config; + } + + public function getConfig(string $key = null) + { + return $key ? Arr::get($this->config, $key) : $this->config; + } + + public function getCurrency(): string + { + return Money::RIAL; + } + + public function setConfig(array $config) + { + $this->config = $config; + } + + public function getAliasDataFields(): array + { + return [ + 'merchantId' => 'api_key', + 'amount' => 'amount', + 'transactionId' => 'id', + 'orderId' => 'order_id', + 'callback' => 'callback', + 'mobile' => 'phone', + 'email' => 'mail', + 'description' => 'desc', + 'name' => 'name', + ]; + } + + public function getMerchantIdData() + { + return Arr::get($this->config, 'api_key'); + } + + /** @inheritDoc */ + public function requestPayment(PendingRequest $pendingRequest, FakeRequest $fakeRequest = null): RequestedPaymentInterface + { + $factory = (new PaymentRequest($pendingRequest)); + + if ($fakeRequest) { + return $factory->fakeFrom($fakeRequest); + } + + return $factory->request(); + } + + /** @inheritDoc */ + public function verifyPayment(PendingRequest $pendingRequest, FakeVerification $fakeVerification = null): CheckedPaymentInterface + { + $factory = (new PaymentVerification($pendingRequest)); + + if ($fakeVerification) { + return $factory->fakeFrom($fakeVerification); + } + + return $factory->verify(); + } + + /** @inheritDoc */ + public function inspectCallbackRequest(PendingRequest $pendingRequest, FakeVerification $fakeVerification = null): void + { + $request = app(CallbackRequest::class); + + if ($fakeVerification) { + $request->setFakeVerification($fakeVerification); + } + + $request->validateCallback(); + + $pendingRequest->orderId($request->getOrderId()); + $pendingRequest->transactionId($request->getTransactionId()); + } +} diff --git a/src/Gateways/IDPay/PaymentRequest.php b/src/Gateways/IDPay/PaymentRequest.php new file mode 100644 index 0000000..fae5d7c --- /dev/null +++ b/src/Gateways/IDPay/PaymentRequest.php @@ -0,0 +1,69 @@ +getException(), + [], + $fakeRequest->getTransactionId(), + 'https://idpay.ir/p/ws-sandbox/'.$fakeRequest->getTransactionId() + ); + } + + public function request(): RequestedPayment + { + $response = Http::asJson()->withHeaders($this->makeHeaders())->post( + $this->getEndpoint('payment'), + $this->pendingRequest->provideForGateway([ + 'callback', + 'orderId', + 'amount', + 'description', + 'email', + 'mobile', + 'name', + ])->filter()->toArray() + ); + + $data = $response->json(); + + // If response has created status, it means the payment has been created + // successfully. + if ($response->status() === Response::HTTP_CREATED) { + return new RequestedPayment(null, [], $data['id'], $data['link']); + } + + // Client errors (4xx) are not guaranteed to be come with error messages. We need to + // check requested payment status too. + if ($response->clientError()) { + return new RequestedPayment( + new GatewayClientException( + $data['error_message'], + $data['error_code'] + ) + ); + } + + // In case of connection issued. It indicates a proper time to switch gateway to + // another provider. + return new RequestedPayment( + new GatewayServerException( + 'Unable to connect to IDPay endpoint due to unexpected error.', + $response->status() + ) + ); + } +} diff --git a/src/Gateways/IDPay/PaymentVerification.php b/src/Gateways/IDPay/PaymentVerification.php new file mode 100644 index 0000000..c09504b --- /dev/null +++ b/src/Gateways/IDPay/PaymentVerification.php @@ -0,0 +1,100 @@ +getStatus() === $fakeVerification::FAILED) { + $status = Status::UNSUCCESSFUL_PAYMENT; + } + + if ($fakeVerification->getStatus() === $fakeVerification::SUCCESSFUL) { + $status = Status::SUCCESSFUL; + } + + if ($fakeVerification->getStatus() === $fakeVerification::ALREADY_VERIFIED) { + $status = Status::ALREADY_VERIFIED; + } + + return new CheckedPayment( + $status, + $fakeVerification->getException(), + [], + $fakeVerification->getOrderId(), + $fakeVerification->getTransactionId(), + $fakeVerification->getReferenceId() + ); + } + + public function verify(): CheckedPayment + { + $response = Http::asJson()->withHeaders($this->makeHeaders())->post( + $this->getEndpoint('payment/verify'), + $this->pendingRequest->provideForGateway([ + 'transactionId', + 'orderId', + ])->filter()->toArray() + ); + + $data = $response->json(); + + // If payment was successful or already verified or paid + if ($response->status() === Response::HTTP_OK) { + return new CheckedPayment( + $data['status'], + null, + [], + $this->pendingRequest->orderId(), + $this->pendingRequest->transactionId(), + $data['payment']['track_id'] + ); + } + + // Client errors (4xx) are not guaranteed to be come with error messages. We need to + // check requested payment status too. + if ($response->clientError()) { + return new CheckedPayment( + $data['error_code'], + new GatewayClientException( + $data['error_message'], + $data['error_code'] + ), + [], + $this->pendingRequest->orderId(), + $this->pendingRequest->transactionId(), + null + ); + } + + // In case of connection issued. It indicates a proper time to switch gateway to + // another provider. + if ($response->serverError()) { + return new CheckedPayment( + $response->status(), + new GatewayServerException( + 'Unable to connect to ZarinPal endpoint due to server error.', + $response->status() + ), + [], + $this->pendingRequest->orderId(), + $this->pendingRequest->transactionId(), + null + ); + } + } +} diff --git a/src/Gateways/IDPay/RequestedPayment.php b/src/Gateways/IDPay/RequestedPayment.php new file mode 100644 index 0000000..4d1ea07 --- /dev/null +++ b/src/Gateways/IDPay/RequestedPayment.php @@ -0,0 +1,48 @@ +transactionId = $transactionId; + $this->transactionUrl = $transactionUrl; + $this->exception = $exception; + $this->messages = $messages; + } + + public function successful(): bool + { + return ! $this->exception; + } + + public function failed(): bool + { + return ! $this->successful(); + } + + /** + * Get the payment URL specified to this payment request. User must be redirected + * there to complete the payment. + * + * @param array $options No option is available for this gateway + * @return string + */ + public function paymentUrl(array $options = []): ?string + { + if ($this->failed()) { + $this->throw(); + } + + return $this->transactionUrl; + } +} diff --git a/src/Gateways/IDPay/Status.php b/src/Gateways/IDPay/Status.php new file mode 100644 index 0000000..20e00ea --- /dev/null +++ b/src/Gateways/IDPay/Status.php @@ -0,0 +1,65 @@ +getConstants()); + + return Arr::get($constants, $value); + } +} diff --git a/src/Gateways/Zarinpal/BaseRequest.php b/src/Gateways/Zarinpal/BaseRequest.php new file mode 100644 index 0000000..d660171 --- /dev/null +++ b/src/Gateways/Zarinpal/BaseRequest.php @@ -0,0 +1,53 @@ +pendingRequest = $pendingRequest; + } + + /** + * Check if request should be sent in a sandbox, testing environment. + * + * @return bool + */ + protected function isSandbox() + { + return $this->pendingRequest->getGateway()->getConfig('sandbox') === true; + } + + /** + * Make environment-aware verification endpoint URL. + * @param string $method + * @return string + */ + protected function getEndpoint(string $method) + { + return $this->getHost()."/pg/rest/WebGate/{$method}.json"; + } + + /** + * Get production or sandbox schema and hostname for requests. + * + * @return string + */ + protected function getHost() + { + $subDomain = $this->isSandbox() ? 'sandbox' : 'www'; + + return "https://{$subDomain}.zarinpal.com"; + } +} diff --git a/src/Gateways/Zarinpal/CallbackRequest.php b/src/Gateways/Zarinpal/CallbackRequest.php new file mode 100644 index 0000000..b0347a1 --- /dev/null +++ b/src/Gateways/Zarinpal/CallbackRequest.php @@ -0,0 +1,39 @@ +fakeVerification) { + $this->merge([ + 'Authority' => $this->fakeVerification->getTransactionId(), + ]); + } + } + + public function rules() + { + return [ + 'Authority' => 'required|string', + ]; + } + + public function messages() + { + return [ + 'Authority' => 'ZarinPal transaction id (Authority)', + ]; + } + + public function getTransactionId() + { + return $this->input('Authority'); + } +} diff --git a/src/Gateways/Zarinpal/CheckedPayment.php b/src/Gateways/Zarinpal/CheckedPayment.php new file mode 100644 index 0000000..673d97e --- /dev/null +++ b/src/Gateways/Zarinpal/CheckedPayment.php @@ -0,0 +1,46 @@ +referenceId = $referenceId; + $this->exception = $exception; + $this->messages = $messages; + $this->status = $status; + $this->transactionId = $transactionId; + } + + public function status() + { + return $this->status; + } + + public function successful(): bool + { + return (int) $this->status === Status::OPERATION_SUCCEED; + } + + public function alreadyVerified(): bool + { + return (int) $this->status === Status::ALREADY_VERIFIED; + } + + public function failed(): bool + { + return (bool) $this->exception; + } +} diff --git a/src/Gateways/Zarinpal/CommonMethods.php b/src/Gateways/Zarinpal/CommonMethods.php deleted file mode 100644 index da03091..0000000 --- a/src/Gateways/Zarinpal/CommonMethods.php +++ /dev/null @@ -1,64 +0,0 @@ -Amount data. - * @param $amount string|integer|float - * @return $this - */ - public function amount($amount) - { - $this->data('Amount', $amount); - - return $this; - } - - /** - * Get production or sandbox schema and hostname for requests. - * - * @return string - * @throws InvalidConfigException - */ - private function getHost() - { - $subDomain = $this->isSandbox() ? 'sandbox' : 'www'; - - return "https://{$subDomain}.zarinpal.com"; - } - - /** - * Check if request should be sent in a sandbox, testing environment. - * - * @return bool - * @throws InvalidConfigException - */ - private function isSandbox() - { - $sandbox = $this->getConfig('sandbox'); - - if ($sandbox === null || $sandbox === false) { - return false; - } elseif ($sandbox === true) { - return true; - } - - throw new InvalidConfigException('sandbox'); - } - - /** - * Get Zarinpal merchant ID from config or overridden one. - * - * @return string - */ - private function getMerchantId() - { - $merchantId = $this->getData('MerchantID'); - - return $merchantId ?? $this->getConfig('merchant_id'); - } -} diff --git a/src/Gateways/Zarinpal/Gateway.php b/src/Gateways/Zarinpal/Gateway.php new file mode 100644 index 0000000..b7247c7 --- /dev/null +++ b/src/Gateways/Zarinpal/Gateway.php @@ -0,0 +1,94 @@ +config = $config; + } + + public function getConfig(string $key = null) + { + return $key ? Arr::get($this->config, $key) : $this->config; + } + + public function getCurrency(): string + { + return Money::TOMAN; + } + + public function setConfig(array $config) + { + $this->config = $config; + } + + public function getAliasDataFields(): array + { + return [ + 'merchantId' => 'MerchantID', + 'amount' => 'Amount', + 'transactionId' => 'Authority', + 'callback' => 'CallbackURL', + 'mobile' => 'Mobile', + 'email' => 'Email', + 'description' => 'Description', + ]; + } + + public function getMerchantIdData() + { + return Arr::get($this->config, 'merchant_id'); + } + + /** @inheritDoc */ + public function requestPayment(PendingRequest $pendingRequest, FakeRequest $fakeRequest = null): RequestedPaymentInterface + { + $factory = (new PaymentRequest($pendingRequest)); + + if ($fakeRequest) { + return $factory->fakeFrom($fakeRequest); + } + + return $factory->request(); + } + + /** @inheritDoc */ + public function verifyPayment(PendingRequest $pendingRequest, FakeVerification $fakeVerification = null): CheckedPaymentInterface + { + $factory = (new PaymentVerification($pendingRequest)); + + if ($fakeVerification) { + return $factory->fakeFrom($fakeVerification); + } + + return $factory->verify(); + } + + /** @inheritDoc */ + public function inspectCallbackRequest(PendingRequest $pendingRequest, FakeVerification $fakeVerification = null): void + { + $request = app(CallbackRequest::class); + + if ($fakeVerification) { + $request->setFakeVerification($fakeVerification); + } + + $request->validateCallback(); + + $pendingRequest->transactionId($request->getTransactionId()); + } +} diff --git a/src/Gateways/Zarinpal/PaymentRequest.php b/src/Gateways/Zarinpal/PaymentRequest.php new file mode 100644 index 0000000..1fa3f0b --- /dev/null +++ b/src/Gateways/Zarinpal/PaymentRequest.php @@ -0,0 +1,66 @@ +getException(), + [], + $fakeRequest->getTransactionId(), + 'example.com' + ); + } + + public function request(): RequestedPayment + { + $response = Http::post( + $this->getEndpoint('PaymentRequest'), + $this->pendingRequest->provideForGateway([ + 'merchantId', + 'callback', + 'description', + 'amount', + 'email', + 'mobile', + ])->filter()->toArray() + ); + + $data = $response->json(); + + // In case of connection issued. It indicates a proper time to switch gateway to + // another provider. + if ($response->serverError()) { + return new RequestedPayment( + new GatewayServerException( + 'Unable to connect to ZarinPal endpoint due to server error.', + $response->status() + ) + ); + } + + // Client errors (4xx) are not guaranteed to be come with error messages. We need to + // check requested payment status too. + if ($response->clientError() || $data['Status'] != Status::OPERATION_SUCCEED) { + return new RequestedPayment( + new GatewayClientException( + Status::toMessage($data['Status']), + $data['Status'] + ), + $data['errors'] ?? [] + ); + } + + return new RequestedPayment(null, [], $data['Authority'], $this->getHost()); + } +} diff --git a/src/Gateways/Zarinpal/PaymentVerification.php b/src/Gateways/Zarinpal/PaymentVerification.php new file mode 100644 index 0000000..a775e61 --- /dev/null +++ b/src/Gateways/Zarinpal/PaymentVerification.php @@ -0,0 +1,88 @@ +getStatus() === $fakeVerification::FAILED) { + $status = Status::FAILED_TRANSACTION; + } + + if ($fakeVerification->getStatus() === $fakeVerification::SUCCESSFUL) { + $status = Status::OPERATION_SUCCEED; + } + + if ($fakeVerification->getStatus() === $fakeVerification::ALREADY_VERIFIED) { + $status = Status::ALREADY_VERIFIED; + } + + return new CheckedPayment( + $status, + $fakeVerification->getException(), + [], + $fakeVerification->getTransactionId(), + $fakeVerification->getReferenceId() + ); + } + + public function verify(): CheckedPayment + { + $response = Http::post( + $this->getEndpoint('PaymentVerification'), + $this->pendingRequest->provideForGateway([ + 'merchantId', + 'transactionId', + 'amount', + ])->filter()->toArray() + ); + + $data = $response->json(); + $status = $data['Status'] ?? null; + + // In case of connection issued. It indicates a proper time to switch gateway to + // another provider. + if ($response->serverError()) { + return new CheckedPayment( + $response->status(), + new GatewayServerException( + 'Unable to connect to ZarinPal endpoint due to server error.', + $response->status() + ), + [], + $this->pendingRequest->transactionId(), + null + ); + } + + // Client errors (4xx) are not guaranteed to be come with error messages. We need to + // check requested payment status too. + if ($response->clientError() || ! in_array($status, [Status::OPERATION_SUCCEED, Status::ALREADY_VERIFIED])) { + return new CheckedPayment( + $status, + new GatewayClientException( + Status::toMessage($status), + $status + ), + $data['errors'] ?? [], + $this->pendingRequest->transactionId(), + null + ); + } + + return new CheckedPayment($status, null, [], $this->pendingRequest->transactionId(), $data['RefID']); + } +} diff --git a/src/Gateways/Zarinpal/RequestedPayment.php b/src/Gateways/Zarinpal/RequestedPayment.php new file mode 100644 index 0000000..082a6d3 --- /dev/null +++ b/src/Gateways/Zarinpal/RequestedPayment.php @@ -0,0 +1,46 @@ +transactionId = $transactionId; + $this->baseUrl = $baseUrl; + $this->exception = $exception; + $this->messages = $messages; + } + + public function successful(): bool + { + return ! $this->exception; + } + + public function failed(): bool + { + return ! $this->successful(); + } + + /** + * Get the payment URL specified to this payment request. User must be redirected + * there to complete the payment. + * + * @param array $options ZarinPal accepts `gateway` option to target a specific bank gateway. Contact with their support for more info. + * @return string + */ + public function paymentUrl(array $options = []): ?string + { + $gateway = isset($options['gateway']) ? "/{$options['gateway']}" : ''; + + return "{$this->baseUrl}/pg/StartPay/{$this->transactionId()}{$gateway}"; + } +} diff --git a/src/Gateways/Zarinpal/Requester.php b/src/Gateways/Zarinpal/Requester.php deleted file mode 100644 index a8e84f0..0000000 --- a/src/Gateways/Zarinpal/Requester.php +++ /dev/null @@ -1,185 +0,0 @@ -setConfig($config); - $this->client = $client; - } - - /** - * Initialize a Requester object on-the-fly. - * @param $config - * @param Client $client - * @return self - */ - public static function make($config, Client $client) - { - return new self($config, $client); - } - - /** - * Set CallbackURL data and override config. - * @param $callbackUrl string - * @return $this - */ - public function callback($callbackUrl) - { - $this->data('CallbackURL', $callbackUrl); - - return $this; - } - - /** - * Set Mobile data. - * @param $mobile string - * @return $this - */ - public function mobile($mobile) - { - $this->data('Mobile', $mobile); - - return $this; - } - - /** - * Set Email data. - * @param $email string - * @return $this - */ - public function email($email) - { - $this->data('Email', $email); - - return $this; - } - - /** - * Set Description data and override config. - * @param $amount - * @return $this - */ - public function description($description) - { - $this->data('Description', $description); - - return $this; - } - - /** - * Request a new payment from gateway. - * @return RequestedPayment If new payment is created and is ready to pay - * @throws \Evryn\LaravelToman\Exceptions\GatewayException If new payment was not created - * @throws InvalidConfigException - */ - public function request(): RequestedPayment - { - try { - $response = $this->client->post( - $this->makeRequestURL(), - [RequestOptions::JSON => $this->makeRequestData()] - ); - } catch (ClientException | ServerException $exception) { - GatewayHelper::zarinFails($exception); - } - - $data = ClientHelper::getResponseData($response); - - $transactionId = Arr::get($data, 'Authority'); - - if (Arr::get($data, 'Status') !== Status::OPERATION_SUCCEED || ! $transactionId) { - GatewayHelper::zarinFails($data); - } - - return new RequestedPayment($transactionId, $this->getPaymentUrlFor($transactionId)); - } - - /** - * Get payable URL for user. - * @param $transactionId - * @return string - * @throws \Evryn\LaravelToman\Exceptions\InvalidConfigException - */ - private function getPaymentUrlFor($transactionId) - { - return $this->getHost()."/pg/StartPay/{$transactionId}"; - } - - /** - * Make environment-aware verification endpoint URL. - * @return string - * @throws InvalidConfigException - */ - private function makeRequestURL() - { - return $this->getHost().'/pg/rest/WebGate/PaymentRequest.json'; - } - - /** - * Make config-aware verification endpoint required data. - * @return array - */ - private function makeRequestData() - { - return array_merge($this->data, [ - 'MerchantID' => $this->getMerchantId(), - 'CallbackURL' => $this->getCallbackUrl(), - 'Description' => $this->getDescription(), - ]); - } - - /** - * Get 'CallbackURL' from data or default one from config if available. - * @return array|mixed|string - */ - private function getCallbackUrl() - { - if ($data = $this->getData('CallbackURL')) { - return $data; - } - - if ($defaultRoute = config('toman.callback_route')) { - return URL::route($defaultRoute); - } - } - - /** - * Get 'Description' from data or default one from config if available. - * @return array|mixed|string - */ - private function getDescription() - { - $description = $this->getData('Description'); - - if (! $description) { - $description = config('toman.description'); - } - - return str_replace(':amount', $this->getData('Amount'), $description); - } -} diff --git a/src/Gateways/Zarinpal/Status.php b/src/Gateways/Zarinpal/Status.php index e2abd11..f655209 100644 --- a/src/Gateways/Zarinpal/Status.php +++ b/src/Gateways/Zarinpal/Status.php @@ -31,7 +31,7 @@ class Status public static function toMessage($status) { - $translationKey = strtolower(self::getConstantName($status)); + $translationKey = strtolower(self::getConstantName($status)) ?: $status; return __("toman::zarinpal.status.$translationKey"); } diff --git a/src/Gateways/Zarinpal/Verifier.php b/src/Gateways/Zarinpal/Verifier.php deleted file mode 100644 index 85d9ca6..0000000 --- a/src/Gateways/Zarinpal/Verifier.php +++ /dev/null @@ -1,100 +0,0 @@ -setConfig($config); - $this->client = $client; - } - - /** - * Initialize a Requester object on-the-fly. - * @param $config - * @param Client $client - * @return self - */ - public static function make($config, Client $client) - { - return new self($config, $client); - } - - /** - * Verify incoming payment callback and get reference ID of transaction if possible. - * @param Request $request Current HTTP request - * @return VerifiedPayment If payment is verified - * @throws GatewayException If payment is not verified - * @throws InvalidConfigException - */ - public function verify(Request $request): VerifiedPayment - { - if ($request->input('Status') !== 'OK') { - throw new GatewayException(Status::toMessage(Status::NOT_PAID), Status::NOT_PAID); - } - - try { - $response = $this->client->post( - $this->makeVerificationURL(), - [RequestOptions::JSON => $this->makeVerificationData($request)] - ); - } catch (ClientException | ServerException $exception) { - GatewayHelper::zarinFails($exception); - } - - $data = ClientHelper::getResponseData($response); - - $status = $data['Status']; - if ($status !== Status::OPERATION_SUCCEED) { - GatewayHelper::zarinFails($data); - } - - return new VerifiedPayment($data['RefID']); - } - - /** - * Make environment-aware verification endpoint URL. - * @return string - * @throws InvalidConfigException - */ - private function makeVerificationURL() - { - return $this->getHost().'/pg/rest/WebGate/PaymentVerification.json'; - } - - /** - * Make config-aware verification endpoint required data. - * @param Request $request - * @return array - */ - private function makeVerificationData(Request $request) - { - return array_merge($this->data, [ - 'MerchantID' => $this->getMerchantId(), - 'Authority' => $request->input('Authority'), - ]); - } -} diff --git a/src/Helpers/Client.php b/src/Helpers/Client.php deleted file mode 100644 index 363e649..0000000 --- a/src/Helpers/Client.php +++ /dev/null @@ -1,23 +0,0 @@ -getResponse(); - } - - return json_decode($response->getBody()->getContents(), true); - } -} diff --git a/src/Helpers/Gateway.php b/src/Helpers/Gateway.php deleted file mode 100644 index 46f7d3d..0000000 --- a/src/Helpers/Gateway.php +++ /dev/null @@ -1,37 +0,0 @@ -app->runningInConsole()) { $this->bootForConsole(); } @@ -39,17 +34,12 @@ public function register() { $this->mergeConfigFrom(self::CONFIG_FILE, 'toman'); - $this->app->singleton(PaymentRequester::class, function ($app) { - return new PaymentRequestManager($app); + $this->app->bind(GatewayInterface::class, function ($app) { + return (new GatewayManager($app))->driver(); }); - $this->app->singleton(PaymentVerifier::class, function ($app) { - return new PaymentVerificationManager($app); - }); - - // Register the Guzzle HTTP client used by drivers to send requests - $this->app->singleton(GuzzleClient::class, function () { - return new Client; + $this->app->singleton(Factory::class, function ($app) { + return new Factory($app->make(GatewayInterface::class)); }); } @@ -62,8 +52,8 @@ public function register() public function provides() { return [ - 'laravel-toman.payment', - GuzzleClient::class, + GatewayInterface::class, + Factory::class, ]; } @@ -74,7 +64,8 @@ public function provides() */ protected function bootForConsole() { - // Publishing the configuration file. + // Make config and translation files publishable via artisan command + $this->publishes([ self::CONFIG_FILE => config_path('toman.php'), ], 'config'); diff --git a/src/Managers/GatewayManager.php b/src/Managers/GatewayManager.php new file mode 100644 index 0000000..c3e782a --- /dev/null +++ b/src/Managers/GatewayManager.php @@ -0,0 +1,42 @@ +validateCurrency($sourceCurrency); + + $this->sourceAmount = $sourceAmount; + $this->sourceCurrency = $sourceCurrency; + } + + public function getSourceCurrency() + { + return $this->sourceCurrency; + } + + public function getSourceValue() + { + return $this->sourceAmount; + } + + public function value(string $targetCurrency) + { + $this->validateCurrency($targetCurrency); + + if ($targetCurrency === $this->sourceCurrency) { + return $this->sourceAmount; + } + + return $this->fromToman( + $this->toToman($this->sourceAmount, $this->sourceCurrency), + $targetCurrency + ); + } + + public static function Toman(int $amount) + { + return new self($amount, self::TOMAN); + } + + public static function Rial(int $amount) + { + return new self($amount, self::RIAL); + } + + public function is(self $money): bool + { + return $this->value(self::TOMAN) === $money->value(self::TOMAN); + } + + protected function toToman(int $amount, string $currency): int + { + switch ($currency) { + case self::TOMAN: + return $amount; + case self::RIAL: + return $amount / 10; + } + } + + protected function fromToman(int $amount, string $currency): int + { + switch ($currency) { + case self::TOMAN: + return $amount; + case self::RIAL: + return $amount * 10; + } + } + + protected function validateCurrency(string $currency) + { + if (! in_array($currency, [self::TOMAN, self::RIAL])) { + throw new \InvalidArgumentException("`{$currency}` is not supported as currency."); + } + } +} diff --git a/src/PendingRequest.php b/src/PendingRequest.php new file mode 100644 index 0000000..8fe7c3b --- /dev/null +++ b/src/PendingRequest.php @@ -0,0 +1,307 @@ +factory = $factory; + $this->gateway = $gateway; + } + + /** + * Get or set data. + * + * @param string|null $key + * @param null $value + * @return $this|array + */ + public function data(string $key = null, $value = null) + { + // Get all data + if (func_num_args() === 0) { + return $this->data; + } + + // Get specific data + if (func_num_args() === 1 && is_string($key)) { + return Arr::get($this->customData, $key) ?? $this->getData($key); + } + + // Replace whole data + if (func_num_args() === 1 && is_array($key)) { + $this->data = $key; + + return $this; + } + + // Set specific data + $this->customData[$key] = $value; + + return $this; + } + + public function provideForGateway(array $fields): Collection + { + return collect($fields)->mapWithKeys(function ($field) { + $value = $this->getData($field); + + if (self::normalizeField($field) === 'amount') { + $value = $this->provideAmount(); + } + + return [$this->getFieldNameForGateway($field) => $value]; + })->merge($this->customData); + } + + /** + * Request a new payment from gateway. + * @return RequestedPaymentInterface + */ + public function request(): RequestedPaymentInterface + { + if ($this->fakeRequest) { + return tap($this->getGateway()->requestPayment($this, $this->fakeRequest), function () { + $this->factory->recordPendingRequest($this); + }); + } + + return $this->getGateway()->requestPayment($this); + } + + /** + * Check a transaction for verification. + * @return CheckedPaymentInterface + */ + public function verify(): CheckedPaymentInterface + { + if ($this->fakeVerification) { + return tap(($this->getGateway()->verifyPayment($this, $this->fakeVerification)), function () { + $this->factory->recordPendingRequest($this); + }); + } + + return $this->getGateway()->verifyPayment($this); + } + + public function stub(FakeRequest $fakeRequest = null, FakeVerification $fakeVerification = null) + { + $this->fakeRequest = $fakeRequest; + $this->fakeVerification = $fakeVerification; + } + + public function inspectCallbackRequest() + { + $this->getGateway()->inspectCallbackRequest($this, $this->fakeVerification); + + return $this; + } + + /** + * Get underlying gateway. + * + * @return GatewayInterface + */ + public function getGateway(): GatewayInterface + { + return $this->gateway; + } + + /** + * Dynamically call the setters. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call(string $method, array $parameters) + { + if ($this->canAccessDataAlias($method)) { + if (isset($parameters[0])) { + return $this->setData($method, $parameters[0]); + } + + return $this->getData($method); + } + + throw new BadMethodCallException(sprintf( + 'Method %s::%s does not exist.', static::class, $method + )); + } + + protected function canAccessDataAlias(string $alias): bool + { + // If it's supported by the gateway, we can use it too + if ($this->getFieldNameForGateway($alias)) { + return true; + } + + return false; + } + + protected function getFieldNameForGateway(string $field) + { + foreach ($this->getGateway()->getAliasDataFields() as $key => $value) { + if (self::normalizeField($key) === self::normalizeField($field)) { + return $value; + } + } + + return null; + } + + /** + * Get or set absolute URL for payment verification callback. + * + * @param string|null $callback + * @return $this|string|null + */ + public function callback(string $callback = null) + { + if (! is_null($callback)) { + $this->setData('callback', $callback); + + return $this; + } + + if ($callback = $this->getRawData('callback')) { + return $callback; + } + + if ($callback = config('toman.callback_route')) { + return route($callback); + } + + return null; + } + + /** + * Get or set amount of the payment. + * + * @param null|int|Money $amount + * @return PendingRequest|Money + */ + public function amount($amount = null) + { + if (is_null($amount)) { + return $this->getRawData('amount'); + } + + if (! $amount instanceof Money) { + $amount = new Money( + $amount, + config('toman.currency') ?: 'toman' + ); + } + + $this->setData('amount', $amount); + + return $this; + } + + protected function provideAmount() + { + if (! $this->amount()) { + return null; + } + + return $this->amount()->value($this->getGateway()->getCurrency()); + } + + /** + * Get or set description. `:amount` will be replaced by the given amount. + * + * @param string|null $description + * @return $this|string|null + */ + public function description(string $description = null) + { + if (! is_null($description)) { + $this->setData('description', $description); + + return $this; + } + + return str_replace( + ':amount', + $this->provideAmount(), + $this->getRawData('description') ?? config('toman.description') + ); + } + + protected function setData(string $field, $value) + { + $this->data[self::normalizeField($field)] = $value; + + return $this; + } + + protected function getData(string $alias) + { + if (method_exists($this, $alias)) { + return $this->{$alias}(); + } + + if ($data = $this->getRawData($alias)) { + return $data; + } + + if (method_exists($this->gateway, $method = 'get'.$alias.'Data')) { + return $this->getGateway()->{$method}(); + } + + return null; + } + + public function getRawData(string $alias) + { + return Arr::get($this->data, self::normalizeField($alias)); + } + + protected static function normalizeField(string $field): string + { + return strtolower($field); + } +} diff --git a/src/Results/RequestedPayment.php b/src/Results/RequestedPayment.php deleted file mode 100644 index b08526b..0000000 --- a/src/Results/RequestedPayment.php +++ /dev/null @@ -1,51 +0,0 @@ -transactionId = $transactionId; - $this->paymentUrl = $paymentUrl; - } - - /** - * Get transaction ID provided by gateway for newly requested payment. You'll - * need it to verify actual payment. - * . - * - * @return mixed - */ - public function getTransactionId() - { - return $this->transactionId; - } - - /** - * Get the payment URL specified to this payment request. User must be redirected - * there to complete the payment. - * - * @return string - */ - public function getPaymentUrl(): string - { - return $this->paymentUrl; - } - - /** - * Redirect user to payment gateway to complete it. - * @return RedirectResponse - */ - public function pay(): RedirectResponse - { - return redirect()->to($this->getPaymentUrl()); - } -} diff --git a/src/Results/VerifiedPayment.php b/src/Results/VerifiedPayment.php deleted file mode 100644 index 362f16d..0000000 --- a/src/Results/VerifiedPayment.php +++ /dev/null @@ -1,18 +0,0 @@ -referenceId = $referenceId; - } - - public function getReferenceId() - { - return $this->referenceId; - } -} diff --git a/tests/CallbackRequestTest.php b/tests/CallbackRequestTest.php new file mode 100644 index 0000000..de91c67 --- /dev/null +++ b/tests/CallbackRequestTest.php @@ -0,0 +1,58 @@ + 'zarinpal', + 'toman.gateways.zarinpal' => [ + 'sandbox' => true, + 'merchant_id' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', + ], + ]); + + Toman::fakeVerification()->successful()->withTransactionId('A123'); + + $pendingRequest = app(CallbackRequest::class)->data('key', 'value'); + + self::assertInstanceOf(PendingRequest::class, $pendingRequest); + self::assertInstanceOf(ZarinpalGateway::class, $pendingRequest->getGateway()); + self::assertEquals([ + 'sandbox' => true, + 'merchant_id' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', + ], $pendingRequest->getGateway()->getConfig()); + } + + /** @test */ + public function resolves_idpay_callback() + { + config([ + 'toman.default' => 'idpay', + 'toman.gateways.idpay' => [ + 'sandbox' => true, + 'api_key' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', + ], + ]); + + Toman::fakeVerification()->successful()->withOrderId('order_1')->withTransactionId('A123'); + + $pendingRequest = app(CallbackRequest::class)->data('key', 'value'); + + self::assertInstanceOf(PendingRequest::class, $pendingRequest); + self::assertInstanceOf(IDPayGateway::class, $pendingRequest->getGateway()); + self::assertEquals([ + 'sandbox' => true, + 'api_key' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', + ], $pendingRequest->getGateway()->getConfig()); + } +} diff --git a/tests/Facades/PaymentRequestTest.php b/tests/Facades/PaymentRequestTest.php deleted file mode 100644 index 8b863fd..0000000 --- a/tests/Facades/PaymentRequestTest.php +++ /dev/null @@ -1,17 +0,0 @@ - 'zarinpal', + 'toman.gateways.zarinpal' => [ + 'sandbox' => true, + 'merchant_id' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', + ], + ]); + + $pendingRequest = Toman::getFacadeRoot()->data('key', 'value'); + + self::assertInstanceOf(PendingRequest::class, $pendingRequest); + self::assertInstanceOf(ZarinpalGateway::class, $pendingRequest->getGateway()); + self::assertEquals([ + 'sandbox' => true, + 'merchant_id' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', + ], $pendingRequest->getGateway()->getConfig()); + } + + /** @test */ + public function resolves_to_configured_idpay_gateway() + { + config([ + 'toman.default' => 'idpay', + 'toman.gateways.idpay' => [ + 'sandbox' => true, + 'api_key' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', + ], + ]); + + $pendingRequest = Toman::getFacadeRoot()->data('key', 'value'); + + self::assertInstanceOf(PendingRequest::class, $pendingRequest); + self::assertInstanceOf(IDPayGateway::class, $pendingRequest->getGateway()); + self::assertEquals([ + 'sandbox' => true, + 'api_key' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', + ], $pendingRequest->getGateway()->getConfig()); + } +} diff --git a/tests/Gateways/DriverTestCase.php b/tests/Gateways/DriverTestCase.php deleted file mode 100644 index cf46319..0000000 --- a/tests/Gateways/DriverTestCase.php +++ /dev/null @@ -1,63 +0,0 @@ -history); - $handlerStack = HandlerStack::create(); - if ($responses) { - $response_mock = new MockHandler(Arr::wrap($responses)); - $handlerStack = HandlerStack::create($response_mock); - } - $handlerStack->push($historyMiddleware); - - return new Client(['handler' => $handlerStack]); - } - - /** - * @return Request - */ - protected function getLastRequest() - { - return Arr::last($this->history)['request']; - } - - protected function getLastRequestURL() - { - return (string)$this->getLastRequest()->getUri(); - } - - protected function getLastRequestData($key = null) - { - $data = json_decode($this->getLastRequest()->getBody()->getContents(), true); - return $key ? Arr::get($data, $key) : $data; - } - - protected function assertLastRequestedDataEquals($expected) - { - self::assertEquals($expected, $this->getLastRequestData()); - } - - protected function assertLastRequestedUrlEquals($expected) - { - self::assertEquals($expected, $this->getLastRequestURL()); - } - - protected function assertDataInRequest($expected, $key) - { - self::assertEquals($expected, $this->getLastRequestData($key)); - } -} diff --git a/tests/Gateways/IDPay/FakeRequestTest.php b/tests/Gateways/IDPay/FakeRequestTest.php new file mode 100644 index 0000000..750f05c --- /dev/null +++ b/tests/Gateways/IDPay/FakeRequestTest.php @@ -0,0 +1,199 @@ +gateway = new Gateway(); + $this->factory = new Factory($this->gateway); + } + + /** @test */ + public function can_fake_successful_request() + { + config([ + 'toman.description' => 'Pay :amount', + 'toman.currency' => 'rial', + ]); + + $this->factory->fakeRequest() + ->withTransactionId('tid_100') + ->successful(); + + $this->gateway->setConfig([ + 'sandbox' => false, + 'api_key' => 'xxxx-yyyyy', + ]); + + $request = $this->factory + ->orderId('order_1') + ->callback('http://example.com/callback') + ->mobile('09350000000') + ->amount(5000) + ->data('CustomData', 'Example') + ->request(); + + $this->factory->assertRequested(function (PendingRequest $request) { + self::assertEquals('xxxx-yyyyy', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('order_1', $request->orderId()); + self::assertEquals('09350000000', $request->mobile()); + self::assertTrue($request->amount()->is(Money::Rial(5000))); + self::assertEquals('Pay 5000', $request->description()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + $request->throw(); + self::assertTrue($request->successful()); + self::assertFalse($request->failed()); + self::assertEquals('tid_100', $request->transactionId()); + self::assertNotEmpty($request->paymentUrl()); + self::assertInstanceOf(RedirectResponse::class, $request->pay()); + } + + /** @test */ + public function can_fake_failed_request() + { + config([ + 'toman.description' => 'Pay :amount', + 'toman.currency' => 'rial', + ]); + + $this->factory->fakeRequest()->failed('Your request has failed.', Status::API_KEY_NOT_FOUND); + + $this->gateway->setConfig([ + 'sandbox' => false, + 'api_key' => 'xxxx-yyyyy', + ]); + + $request = $this->factory + ->orderId('order_1') + ->callback('http://example.com/callback') + ->mobile('09350000000') + ->amount(5000) + ->data('CustomData', 'Example') + ->request(); + + $this->factory->assertRequested(function (PendingRequest $request) { + self::assertEquals('xxxx-yyyyy', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('order_1', $request->orderId()); + self::assertEquals('09350000000', $request->mobile()); + self::assertTrue($request->amount()->is(Money::Rial(5000))); + self::assertEquals('Pay 5000', $request->description()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + self::assertFalse($request->successful()); + self::assertTrue($request->failed()); + + try { + $request->throw(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + self::assertEquals(Status::API_KEY_NOT_FOUND, $e->getCode()); + self::assertEquals('Your request has failed.', $e->getMessage()); + self::assertEquals('Your request has failed.', $request->message()); + self::assertEquals(['Your request has failed.'], $request->messages()); + } + + try { + $request->transactionId(); + self::fail(); + } catch (GatewayException $e) { + } + + try { + $request->pay(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + } + + try { + $request->paymentUrl(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + } + } + + /** @test */ + public function assertion_fails_when_truth_check_is_false() + { + $this->factory->fakeRequest() + ->withTransactionId('tid_100') + ->successful(); + + $this->factory->request(); + + $this->expectException(AssertionFailedError::class); + + $this->factory->assertRequested(function (PendingRequest $request) { + return false; + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\IDPay\Provider::fakeRialBasedAmountProvider() + */ + public function can_assert_correct_fake_amount_in_currencies($configCurrency, $actualAmount, Money $expectedAmount) + { + config([ + 'toman.currency' => $configCurrency, + ]); + + $this->factory->fakeRequest() + ->withTransactionId('tid_100') + ->successful(); + + $this->factory->amount($actualAmount)->request(); + + $this->factory->assertRequested(function (PendingRequest $request) use ($expectedAmount) { + return $request->amount()->is($expectedAmount); + }); + } + + /** @test */ + public function can_not_assert_incorrect_fake_amount_in_currencies() + { + config([ + 'toman.currency' => 'rial', + ]); + + $this->factory->fakeRequest() + ->withTransactionId('tid_100') + ->successful(); + + $this->factory->amount(10)->request(); + + $this->expectException(AssertionFailedError::class); + + $this->factory->assertRequested(function (PendingRequest $request) { + return $request->amount()->is(Money::Toman(10)); + }); + } +} diff --git a/tests/Gateways/IDPay/FakeVerificationTest.php b/tests/Gateways/IDPay/FakeVerificationTest.php new file mode 100644 index 0000000..404eb63 --- /dev/null +++ b/tests/Gateways/IDPay/FakeVerificationTest.php @@ -0,0 +1,245 @@ +gateway = new Gateway(); + $this->factory = new Factory($this->gateway); + $this->callbackRequest = new CallbackRequest($this->factory); + } + + /** @test */ + public function can_fake_successful_verification() + { + $this->factory->fakeVerification() + ->withOrderId('order_1') + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $this->gateway->setConfig([ + 'api_key' => 'xxxxx-yyyyy-zzzzz', + ]); + + $verification = $this->factory + ->orderId('order_1') + ->transactionId('tid_100') + ->callback('http://example.com/callback') + ->data('CustomData', 'Example') + ->verify(); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + self::assertEquals('order_1', $request->orderId()); + self::assertEquals('tid_100', $request->transactionId()); + self::assertEquals('xxxxx-yyyyy-zzzzz', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + self::assertTrue($verification->successful()); + self::assertFalse($verification->alreadyVerified()); + self::assertFalse($verification->failed()); + + $verification->throw(); + self::assertEquals('order_1', $verification->orderId()); + self::assertEquals('tid_100', $verification->transactionId()); + self::assertEquals('ref_100', $verification->referenceId()); + } + + /** @test */ + public function can_fake_already_verified_verification() + { + $this->factory->fakeVerification() + ->withOrderId('order_1') + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->alreadyVerified(); + + $this->gateway->setConfig([ + 'api_key' => 'xxxxx-yyyyy-zzzzz', + ]); + + $verification = $this->factory + ->orderId('order_1') + ->transactionId('tid_100') + ->callback('http://example.com/callback') + ->data('CustomData', 'Example') + ->verify(); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + self::assertEquals('order_1', $request->orderId()); + self::assertEquals('tid_100', $request->transactionId()); + self::assertEquals('xxxxx-yyyyy-zzzzz', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + self::assertFalse($verification->successful()); + self::assertTrue($verification->alreadyVerified()); + self::assertFalse($verification->failed()); + + $verification->throw(); + self::assertEquals('order_1', $verification->orderId()); + self::assertEquals('tid_100', $verification->transactionId()); + self::assertEquals('ref_100', $verification->referenceId()); + } + + /** @test */ + public function can_fake_failed_verification() + { + $this->factory->fakeVerification() + ->withOrderId('order_1') + ->withTransactionId('tid_100') + ->failed('Payment has failed.', Status::UNSUCCESSFUL_PAYMENT); + + $this->gateway->setConfig([ + 'api_key' => 'xxxxx-yyyyy-zzzzz', + ]); + + $verification = $this->factory + ->orderId('order_1') + ->transactionId('tid_100') + ->callback('http://example.com/callback') + ->data('CustomData', 'Example') + ->verify(); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + self::assertEquals('order_1', $request->orderId()); + self::assertEquals('tid_100', $request->transactionId()); + self::assertEquals('xxxxx-yyyyy-zzzzz', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + self::assertFalse($verification->successful()); + self::assertFalse($verification->alreadyVerified()); + self::assertTrue($verification->failed()); + + self::assertEquals('order_1', $verification->orderId()); + self::assertEquals('tid_100', $verification->transactionId()); + + try { + $verification->throw(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + self::assertEquals(Status::UNSUCCESSFUL_PAYMENT, $e->getCode()); + self::assertEquals('Payment has failed.', $e->getMessage()); + self::assertEquals('Payment has failed.', $verification->message()); + self::assertEquals(['Payment has failed.'], $verification->messages()); + } + + try { + $verification->referenceId(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + } + } + + /** @test */ + public function callback_sets_proper_values_from_faked_verification() + { + $this->factory->fakeVerification() + ->withOrderId('order_1') + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $verification = $this->callbackRequest->validateResolved(); + + self::assertEquals('order_1', $verification->orderId()); + self::assertEquals('tid_100', $verification->transactionId()); + } + + /** @test */ + public function assertion_fails_when_truth_check_is_false() + { + $this->factory->fakeVerification() + ->withOrderId('order_1') + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $this->factory->verify(); + + $this->expectException(AssertionFailedError::class); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + return false; + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\IDPay\Provider::fakeRialBasedAmountProvider() + */ + public function can_assert_correct_fake_amount_in_currencies($configCurrency, $actualAmount, Money $expectedAmount) + { + config([ + 'toman.currency' => $configCurrency, + ]); + + $this->factory->fakeVerification() + ->withOrderId('order_1') + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $this->factory->amount($actualAmount)->verify(); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) use ($expectedAmount) { + return $request->amount()->is($expectedAmount); + }); + } + + /** @test */ + public function can_not_assert_incorrect_fake_amount_in_currencies() + { + config([ + 'toman.currency' => 'rial', + ]); + + $this->factory->fakeVerification() + ->withOrderId('order_1') + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $this->factory->amount(10)->verify(); + + $this->expectException(AssertionFailedError::class); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + return $request->amount()->is(Money::Toman(10)); + }); + } +} diff --git a/tests/Gateways/IDPay/PendingRequestTest.php b/tests/Gateways/IDPay/PendingRequestTest.php new file mode 100644 index 0000000..5f479eb --- /dev/null +++ b/tests/Gateways/IDPay/PendingRequestTest.php @@ -0,0 +1,103 @@ +gateway = new Gateway(); + + $this->factory = new Factory($this->gateway); + } + + /** @test */ + public function can_set_merchant_id_elegantly() + { + $this->gateway->setConfig(['api_key' => 'AAA']); + self::assertEquals('AAA', $this->factory->merchantId()); + + self::assertEquals( + 'BBB', + $this->factory->merchantId('BBB')->merchantId() + ); + } + + /** @test */ + public function can_set_callback_url_elegantly() + { + $this->app['router']->get('/callback1')->name('payment.callback'); + + config(['toman.callback_route' => 'payment.callback']); + self::assertEquals(route('payment.callback'), $this->factory->callback()); + + self::assertEquals( + 'https://example.com', + $this->factory->callback('https://example.com')->callback() + ); + } + + /** @test */ + public function can_set_description_elegantly() + { + config(['toman.description' => 'Paying :amount for invoice 1']); + self::assertEquals( + 'Paying 5000 for invoice 1', + $this->factory->amount(Money::Rial(5000))->description('Paying :amount for invoice 1')->description() + ); + + self::assertEquals( + 'Paying 5000 for invoice 2', + $this->factory->description('Paying :amount for invoice 2')->amount(Money::Rial(5000))->description() + ); + } + + /** @test */ + public function can_set_mobile_elegantly() + { + self::assertEquals( + '09350000000', + $this->factory->mobile('09350000000')->mobile() + ); + } + + /** @test */ + public function can_set_email_elegantly() + { + self::assertEquals( + 'amirreza@exmaple.com', + $this->factory->email('amirreza@exmaple.com')->email() + ); + } + + /** @test */ + public function can_set_name_elegantly() + { + self::assertEquals( + 'amirreza', + $this->factory->name('amirreza')->name() + ); + } + + /** @test */ + public function can_set_order_id_elegantly() + { + self::assertEquals( + 'order10000', + $this->factory->orderId('order10000')->orderId() + ); + } +} diff --git a/tests/Gateways/IDPay/Provider.php b/tests/Gateways/IDPay/Provider.php new file mode 100644 index 0000000..4d1a799 --- /dev/null +++ b/tests/Gateways/IDPay/Provider.php @@ -0,0 +1,123 @@ + [true], + 'Production' => [false], + ]; + } + + public static function clientErrorProvider() + { + return [ + [403, Status::USER_IS_BLOCKED], + [403, Status::API_KEY_NOT_FOUND], + [403, Status::UNMATCHED_IP], + [403, Status::UNVERIFIED_WEB_SERVICE], + [403, Status::UNVERIFIED_BANK_ACCOUNT], + [404, Status::WEB_SERVICE_NOT_FOUND], + [401, Status::INVALID_WEB_SERVICE], + [403, Status::INACTIVE_BANK_ACCOUNT], + [406, Status::TRANSACTION_ID_REQUIRED], + [406, Status::ORDER_ID_REQUIRED], + [406, Status::AMOUNT_REQUIRED], + [406, Status::AMOUNT_MIN], + [406, Status::AMOUNT_MAX], + [406, Status::AMOUNT_NOT_ALLOWED], + [406, Status::CALLBACK_REQUIRED], + [406, Status::UNMATCHED_CALLBACK_DOMAIN], + [406, Status::INVALID_TRANSACTION_STATUS], + [406, Status::INVALID_PAYMENT_CREATION_DATE], + [406, Status::INVALID_PAYMENT_RECEIPT_DATE], + [405, Status::TRANSACTION_NOT_CREATED], + [400, Status::NO_RESULT_FOR_VERIFICATION], + [405, Status::UNABLE_TO_VERIFY], + [405, Status::PAYMENT_VERIFICATION_DATE_EXPIRED], + ]; + } + + public static function rialBasedAmountProvider() + { + return [ + // [ + // config value, + // input amount + // expected amount value in Rial + // ], + 'Default currency is Toman (via config)' => [ + 'toman', + 5, + 50, + ], + 'Default currency is Rial (via config)' => [ + 'rial', + 50, + 50, + ], + 'Default currency is Toman (when no config is set)' => [ + null, + 50, + 500, + ], + 'Currency is Rial (overridden)' => [ + 'toman', + Money::Rial(50), + 50, + ], + 'Currency is Toman (overridden)' => [ + 'toman', + Money::Toman(5), + 50, + ], + ]; + } + + public static function fakeRialBasedAmountProvider() + { + return [ + 'Default currency is Toman (via config)' => [ + 'toman', + 5, + Money::Rial(50), + ], + 'Default currency is Rial (via config)' => [ + 'rial', + 50, + Money::Rial(50), + ], + 'Default currency is Toman (when no config is set)' => [ + null, + 50, + Money::Rial(500), + ], + 'Currency is Rial (overridden) compared with Toman' => [ + 'toman', + Money::Rial(50), + Money::Toman(5), + ], + 'Currency is Toman (overridden) compared with Toman' => [ + 'toman', + Money::Toman(5), + Money::Toman(5), + ], + 'Currency is Toman (overridden) compared with Rial' => [ + 'toman', + Money::Toman(5), + Money::Rial(50), + ], + 'Currency is Rial (overridden) compared with Rial' => [ + 'toman', + Money::Rial(50), + Money::Rial(50), + ], + ]; + } +} diff --git a/tests/Gateways/IDPay/RequestTest.php b/tests/Gateways/IDPay/RequestTest.php new file mode 100644 index 0000000..798b6c4 --- /dev/null +++ b/tests/Gateways/IDPay/RequestTest.php @@ -0,0 +1,219 @@ +gateway = new Gateway(); + + $this->factory = new Factory($this->gateway); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\IDPay\Provider::endpointProvider() + */ + public function can_request_payment(bool $sandbox) + { + // By sending a request to create a new IDPay transaction, + // we need to ensure that it sends request to proper endpoint, with correct + // data and amount. + // We also need to check that created payment can be redirected to default gateway + // or the specific one. + + config([ + 'toman.currency' => 'rial', + ]); + + Http::fake([ + 'https://api.idpay.ir/v1.1/payment' => Http::response([ + 'id' => 'tid1000', + 'link' => 'https://idpay.ir/p/ws-sandbox/tid1000', + ], 201), + ]); + + $this->gateway->setConfig([ + 'sandbox' => $sandbox, + 'api_key' => 'xxxx-xxxx-xxxx-xxxx', + ]); + + $gateway = $this->factory + ->callback('https://example.com/callback') + ->orderId('order1000') + ->amount(1500) + ->description('Payment for :amount.') + ->name('Amirreza') + ->mobile('09350000000') + ->email('amirreza@example.com'); + + tap($gateway->request(), function (RequestedPayment $request) use ($sandbox) { + Http::assertSent(function (Request $request) use ($sandbox) { + return ($sandbox ? $request->header('X-SANDBOX')[0] === '1' : ! $request->hasHeader('X-SANDBOX')) + && $request->header('X-API-KEY')[0] === 'xxxx-xxxx-xxxx-xxxx' + && $request->method() === 'POST' + && $request->isJson() + && $request->url() === 'https://api.idpay.ir/v1.1/payment' + && $request['callback'] === 'https://example.com/callback' + && $request['order_id'] === 'order1000' + && $request['amount'] == 1500 + && $request['name'] === 'Amirreza' + && $request['phone'] === '09350000000' + && $request['mail'] === 'amirreza@example.com' + && $request['desc'] === 'Payment for 1500.'; + }); + + // Since request is successful, we need to ensure that nothing can be + // thrown and determiners are correct + $request->throw(); + self::assertTrue($request->successful()); + self::assertFalse($request->failed()); + + self::assertEquals('tid1000', $request->transactionId()); + + $redirectDefault = $request->pay(); + self::assertInstanceOf(RedirectResponse::class, $redirectDefault); + self::assertEquals('https://idpay.ir/p/ws-sandbox/tid1000', $redirectDefault->getTargetUrl()); + self::assertEquals('https://idpay.ir/p/ws-sandbox/tid1000', $request->paymentUrl()); + }); + } + + /** @test */ + public function fails_with_server_error() + { + // When requesting a valid payment result in 5xx error, we consider this as an + // issue in gateway-side (server) and expect an expect API to provide proper results + + Http::fake([ + 'https://api.idpay.ir/v1.1/payment' => Http::response(null, 555), + ]); + + tap($this->factory->request(), function (RequestedPayment $request) { + self::assertFalse($request->successful()); + self::assertTrue($request->failed()); + + try { + $request->throw(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + self::assertEquals(555, $exception->getCode()); + self::assertEquals($exception->getMessage(), $request->message()); + self::assertEquals($exception->getCode(), $request->status()); + } + + try { + $request->transactionId(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + } + + try { + $request->paymentUrl(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + } + + try { + $request->pay(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + } + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\IDPay\Provider::clientErrorProvider() + */ + public function fails_with_client_error($httpStatus, $statusCode) + { + // When requesting a valid payment result in 4xx error, with given error messages, + // we consider this as an issue in merchant-side (client) and expect an expect API + // to provide proper results + + Http::fake([ + 'https://api.idpay.ir/v1.1/payment' => Http::response([ + 'error_code' => $statusCode, + 'error_message' => 'Something failed ...', + ], $httpStatus), + ]); + + tap($this->factory->request(), function (RequestedPayment $request) use ($statusCode) { + self::assertFalse($request->successful()); + self::assertTrue($request->failed()); + + try { + $request->throw(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayClientException $exception) { + self::assertEquals($statusCode, $exception->getCode()); + self::assertEquals($exception->getCode(), $request->status()); + + self::assertEquals('Something failed ...', $exception->getMessage()); + self::assertEquals('Something failed ...', $request->message()); + self::assertEquals(['Something failed ...'], $request->messages()); + } + + try { + $request->transactionId(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + + try { + $request->paymentUrl(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + + try { + $request->pay(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\IDPay\Provider::rialBasedAmountProvider() + */ + public function can_set_amount_in_different_currencies($configCurrency, $actualAmount, $expectedAmountValue) + { + config([ + 'toman.currency' => $configCurrency, + ]); + + Http::fake([ + 'https://api.idpay.ir/v1.1/payment' => Http::response([ + 'id' => 'tid1000', + 'link' => 'https://idpay.ir/p/ws-sandbox/tid1000', + ], 201), + ]); + + $this->factory->amount($actualAmount)->request(); + + Http::assertSent(function (Request $request) use ($expectedAmountValue) { + return $request['amount'] == $expectedAmountValue; + }); + } +} diff --git a/tests/Gateways/IDPay/VerificationTest.php b/tests/Gateways/IDPay/VerificationTest.php new file mode 100644 index 0000000..9556809 --- /dev/null +++ b/tests/Gateways/IDPay/VerificationTest.php @@ -0,0 +1,281 @@ +gateway = new Gateway(); + $this->factory = new Factory($this->gateway); + $this->callbackRequest = new CallbackRequest($this->factory); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\IDPay\Provider::endpointProvider() + */ + public function can_verify_manually(bool $sandbox) + { + Http::fake([ + 'https://api.idpay.ir/v1.1/payment/verify' => Http::response([ + 'status' => 100, + 'payment' => [ + 'track_id' => 'rid_1000', + ], + ], 200), + ]); + + $this->gateway->setConfig([ + 'sandbox' => $sandbox, + 'api_key' => 'xxxx-xxxx-xxxx-xxxx', + ]); + + tap( + $this->factory + ->orderId('order_1') + ->transactionId('tid_1') + ->verify(), + function (CheckedPayment $request) use ($sandbox) { + Http::assertSent(function (Request $request) use ($sandbox) { + return ($sandbox ? $request->header('X-SANDBOX')[0] === '1' : ! $request->hasHeader('X-SANDBOX')) + && $request->header('X-API-KEY')[0] === 'xxxx-xxxx-xxxx-xxxx' + && $request->method() === 'POST' + && $request->isJson() + && $request->url() === 'https://api.idpay.ir/v1.1/payment/verify' + && $request['order_id'] === 'order_1' + && $request['id'] == 'tid_1'; + }); + + // Since request is successful, we need to ensure that nothing can be + // thrown and determiners are correct + $request->throw(); + self::assertTrue($request->successful()); + self::assertFalse($request->alreadyVerified()); + self::assertFalse($request->failed()); + + self::assertEquals('order_1', $request->orderId()); + self::assertEquals('tid_1', $request->transactionId()); + self::assertEquals('rid_1000', $request->referenceId()); + } + ); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\IDPay\Provider::endpointProvider() + */ + public function can_verify_callback_request(bool $sandbox) + { + Http::fake([ + 'https://api.idpay.ir/v1.1/payment/verify' => Http::response([ + 'status' => 100, + 'payment' => [ + 'track_id' => 'rid_1000', + ], + ], 200), + ]); + + request()->merge([ + 'id' => 'tid_1', + 'order_id' => 'order_1', + ]); + + $this->gateway->setConfig([ + 'sandbox' => $sandbox, + 'api_key' => 'xxxx-xxxx-xxxx-xxxx', + ]); + + $gateway = $this->callbackRequest->validateResolved(); + + tap($gateway->verify(), function (CheckedPayment $request) use ($sandbox) { + Http::assertSent(function (Request $request) use ($sandbox) { + return ($sandbox ? $request->header('X-SANDBOX')[0] === '1' : ! $request->hasHeader('X-SANDBOX')) + && $request->header('X-API-KEY')[0] === 'xxxx-xxxx-xxxx-xxxx' + && $request->method() === 'POST' + && $request->isJson() + && $request->url() === 'https://api.idpay.ir/v1.1/payment/verify' + && $request['order_id'] === 'order_1' + && $request['id'] == 'tid_1'; + }); + + $request->throw(); + self::assertTrue($request->successful()); + self::assertFalse($request->alreadyVerified()); + self::assertFalse($request->failed()); + + self::assertEquals('order_1', $request->orderId()); + self::assertEquals('tid_1', $request->transactionId()); + self::assertEquals('rid_1000', $request->referenceId()); + }); + } + + public static function badId() + { + return [ + 'Empty' => [''], + 'Array' => [['some-id']], + ]; + } + + /** + * @test + * @dataProvider badId + */ + public function validates_callback_transaction_id($value) + { + Http::fake(); + + request()->merge([ + 'id' => $value, + 'order_id' => 'order_1', + ]); + + $this->expectException(ValidationException::class); + + $this->callbackRequest->validateResolved(); + } + + /** + * @test + * @dataProvider badId + */ + public function validates_callback_order_id($value) + { + Http::fake(); + + request()->merge([ + 'id' => 'tid_1000', + 'order_id' => $value, + ]); + + $this->expectException(ValidationException::class); + + $this->callbackRequest->validateResolved(); + } + + /** @test */ + public function can_determine_if_transaction_has_already_been_verified() + { + Http::fake([ + 'https://api.idpay.ir/v1.1/payment/verify' => Http::response([ + 'status' => 101, + 'payment' => [ + 'track_id' => 'rid_1000', + ], + ], 200), + ]); + + $gateway = $this->factory + ->orderId('order_1') + ->transactionId('tid_1000'); + + tap($gateway->verify(), function (CheckedPayment $request) { + $request->throw(); + self::assertFalse($request->successful()); + self::assertTrue($request->alreadyVerified()); + self::assertFalse($request->failed()); + + self::assertEquals('order_1', $request->orderId()); + self::assertEquals('tid_1000', $request->transactionId()); + self::assertEquals('rid_1000', $request->referenceId()); + }); + } + + /** @test */ + public function fails_with_server_error() + { + Http::fake([ + 'https://api.idpay.ir/v1.1/payment/verify' => Http::response(null, 555), + ]); + + $gateway = $this->factory->transactionId('A0000012345'); + + tap($gateway->verify(), function (CheckedPayment $verification) { + self::assertFalse($verification->successful()); + self::assertFalse($verification->alreadyVerified()); + self::assertTrue($verification->failed()); + + try { + $verification->throw(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + self::assertEquals(555, $exception->getCode()); + self::assertEquals($exception->getMessage(), $verification->message()); + self::assertEquals($exception->getCode(), $verification->status()); + } + + self::assertNotEmpty($verification->transactionId()); + + try { + $verification->referenceId(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + } + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\IDPay\Provider::clientErrorProvider() + */ + public function fails_with_client_error($httpStatus, $statusCode) + { + Http::fake([ + 'https://api.idpay.ir/v1.1/payment/verify' => Http::response([ + 'error_code' => $statusCode, + 'error_message' => 'Something failed ...', + ], $httpStatus), + ]); + + $gateway = $this->factory->transactionId('A0000012345'); + + tap($gateway->verify(), function (CheckedPayment $verification) use ($statusCode) { + self::assertFalse($verification->successful()); + self::assertFalse($verification->alreadyVerified()); + self::assertTrue($verification->failed()); + + try { + $verification->throw(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + self::assertEquals($statusCode, $exception->getCode()); + self::assertEquals($exception->getCode(), $verification->status()); + + self::assertEquals('Something failed ...', $exception->getMessage()); + self::assertEquals('Something failed ...', $verification->message()); + self::assertEquals(['Something failed ...'], $verification->messages()); + } + + self::assertNotEmpty($verification->transactionId()); + + try { + $verification->referenceId(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayClientException $exception) { + } + }); + } +} diff --git a/tests/Gateways/Zarinpal/FakeRequestTest.php b/tests/Gateways/Zarinpal/FakeRequestTest.php new file mode 100644 index 0000000..534d213 --- /dev/null +++ b/tests/Gateways/Zarinpal/FakeRequestTest.php @@ -0,0 +1,195 @@ +gateway = new Gateway(); + $this->factory = new Factory($this->gateway); + } + + /** @test */ + public function can_fake_successful_request() + { + config([ + 'toman.description' => 'Pay :amount', + 'toman.currency' => 'toman', + ]); + + $this->factory->fakeRequest() + ->withTransactionId('tid_100') + ->successful(); + + $this->gateway->setConfig([ + 'sandbox' => false, + 'merchant_id' => 'xxxx-yyyyy', + ]); + + $request = $this->factory + ->callback('http://example.com/callback') + ->mobile('09350000000') + ->amount(5000) + ->data('CustomData', 'Example') + ->request(); + + $this->factory->assertRequested(function (PendingRequest $request) { + self::assertEquals('xxxx-yyyyy', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('09350000000', $request->mobile()); + self::assertTrue($request->amount()->is(Money::Toman(5000))); + self::assertEquals('Pay 5000', $request->description()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + $request->throw(); + self::assertTrue($request->successful()); + self::assertFalse($request->failed()); + self::assertEquals('tid_100', $request->transactionId()); + self::assertNotEmpty($request->paymentUrl()); + self::assertInstanceOf(RedirectResponse::class, $request->pay()); + } + + /** @test */ + public function can_fake_failed_request() + { + config([ + 'toman.description' => 'Pay :amount', + 'toman.currency' => 'toman', + ]); + + $this->factory->fakeRequest()->failed('Your request has failed.', Status::WRONG_IP_OR_MERCHANT_ID); + + $this->gateway->setConfig([ + 'sandbox' => false, + 'merchant_id' => 'xxxx-yyyyy', + ]); + + $request = $this->factory + ->callback('http://example.com/callback') + ->mobile('09350000000') + ->amount(5000) + ->data('CustomData', 'Example') + ->request(); + + $this->factory->assertRequested(function (PendingRequest $request) { + self::assertEquals('xxxx-yyyyy', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('09350000000', $request->mobile()); + self::assertTrue($request->amount()->is(Money::Toman(5000))); + self::assertEquals('Pay 5000', $request->description()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + self::assertFalse($request->successful()); + self::assertTrue($request->failed()); + + try { + $request->throw(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + self::assertEquals(Status::WRONG_IP_OR_MERCHANT_ID, $e->getCode()); + self::assertEquals('Your request has failed.', $e->getMessage()); + self::assertEquals('Your request has failed.', $request->message()); + self::assertEquals(['Your request has failed.'], $request->messages()); + } + + try { + $request->transactionId(); + self::fail(); + } catch (GatewayException $e) { + } + + try { + $request->pay(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + } + + try { + $request->paymentUrl(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + } + } + + /** @test */ + public function assertion_fails_when_truth_check_is_false() + { + $this->factory->fakeRequest() + ->withTransactionId('tid_100') + ->successful(); + + $this->factory->request(); + + $this->expectException(AssertionFailedError::class); + + $this->factory->assertRequested(function (PendingRequest $request) { + return false; + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::fakeTomanBasedAmountProvider() + */ + public function can_assert_correct_fake_amount_in_currencies($configCurrency, $actualAmount, Money $expectedAmount) + { + config([ + 'toman.currency' => $configCurrency, + ]); + + $this->factory->fakeRequest() + ->withTransactionId('tid_100') + ->successful(); + + $this->factory->amount($actualAmount)->request(); + + $this->factory->assertRequested(function (PendingRequest $request) use ($expectedAmount) { + return $request->amount()->is($expectedAmount); + }); + } + + /** @test */ + public function can_not_assert_incorrect_fake_amount_in_currencies() + { + config([ + 'toman.currency' => 'toman', + ]); + + $this->factory->fakeRequest() + ->withTransactionId('tid_100') + ->successful(); + + $this->factory->amount(10)->request(); + + $this->expectException(AssertionFailedError::class); + + $this->factory->assertRequested(function (PendingRequest $request) { + return $request->amount()->is(Money::Rial(10)); + }); + } +} diff --git a/tests/Gateways/Zarinpal/FakeVerificationTest.php b/tests/Gateways/Zarinpal/FakeVerificationTest.php new file mode 100644 index 0000000..bd13bae --- /dev/null +++ b/tests/Gateways/Zarinpal/FakeVerificationTest.php @@ -0,0 +1,246 @@ +gateway = new Gateway(); + $this->factory = new Factory($this->gateway); + $this->callbackRequest = new CallbackRequest($this->factory); + } + + /** @test */ + public function can_fake_successful_verification() + { + config([ + 'toman.currency' => 'toman', + ]); + + $this->factory->fakeVerification() + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $this->gateway->setConfig([ + 'merchant_id' => 'xxxxx-yyyyy-zzzzz', + ]); + + $verification = $this->factory + ->amount(5000) + ->transactionId('tid_100') + ->callback('http://example.com/callback') + ->data('CustomData', 'Example') + ->verify(); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + self::assertTrue($request->amount()->is(Money::Toman(5000))); + self::assertEquals('tid_100', $request->transactionId()); + self::assertEquals('xxxxx-yyyyy-zzzzz', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + self::assertTrue($verification->successful()); + self::assertFalse($verification->alreadyVerified()); + self::assertFalse($verification->failed()); + + $verification->throw(); + self::assertEquals('tid_100', $verification->transactionId()); + self::assertEquals('ref_100', $verification->referenceId()); + } + + /** @test */ + public function can_fake_already_verified_verification() + { + config([ + 'toman.currency' => 'toman', + ]); + + $this->factory->fakeVerification() + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->alreadyVerified(); + + $this->gateway->setConfig([ + 'merchant_id' => 'xxxxx-yyyyy-zzzzz', + ]); + + $verification = $this->factory + ->amount(5000) + ->transactionId('tid_100') + ->callback('http://example.com/callback') + ->data('CustomData', 'Example') + ->verify(); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + self::assertTrue($request->amount()->is(Money::Toman(5000))); + self::assertEquals('tid_100', $request->transactionId()); + self::assertEquals('xxxxx-yyyyy-zzzzz', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + self::assertFalse($verification->successful()); + self::assertTrue($verification->alreadyVerified()); + self::assertFalse($verification->failed()); + + $verification->throw(); + self::assertEquals('tid_100', $verification->transactionId()); + self::assertEquals('ref_100', $verification->referenceId()); + } + + /** @test */ + public function can_fake_failed_verification() + { + config([ + 'toman.currency' => 'toman', + ]); + + $this->factory->fakeVerification() + ->withTransactionId('tid_100') + ->failed('Payment has failed.', Status::FAILED_TRANSACTION); + + $this->gateway->setConfig([ + 'merchant_id' => 'xxxxx-yyyyy-zzzzz', + ]); + + $verification = $this->factory + ->amount(5000) + ->transactionId('tid_100') + ->callback('http://example.com/callback') + ->data('CustomData', 'Example') + ->verify(); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + self::assertTrue($request->amount()->is(Money::Toman(5000))); + self::assertEquals('tid_100', $request->transactionId()); + self::assertEquals('xxxxx-yyyyy-zzzzz', $request->merchantId()); + self::assertEquals('http://example.com/callback', $request->callback()); + self::assertEquals('Example', $request->data('CustomData')); + + return true; + }); + + self::assertFalse($verification->successful()); + self::assertFalse($verification->alreadyVerified()); + self::assertTrue($verification->failed()); + + self::assertEquals('tid_100', $verification->transactionId()); + + try { + $verification->throw(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + self::assertEquals(Status::FAILED_TRANSACTION, $e->getCode()); + self::assertEquals('Payment has failed.', $e->getMessage()); + self::assertEquals('Payment has failed.', $verification->message()); + self::assertEquals(['Payment has failed.'], $verification->messages()); + } + + try { + $verification->referenceId(); + self::fail('Nothing is thrown.'); + } catch (GatewayException $e) { + } + } + + /** @test */ + public function callback_sets_proper_values_from_faked_verification() + { + $this->factory->fakeVerification() + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $verification = $this->callbackRequest->validateResolved(); + + self::assertEquals('tid_100', $verification->transactionId()); + } + + /** @test */ + public function assertion_fails_when_truth_check_is_false() + { + $this->factory->fakeVerification() + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $this->factory->verify(); + + $this->expectException(AssertionFailedError::class); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + return false; + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::fakeTomanBasedAmountProvider() + */ + public function can_assert_correct_fake_amount_in_currencies($configCurrency, $actualAmount, Money $expectedAmount) + { + config([ + 'toman.currency' => $configCurrency, + ]); + + $this->factory->fakeVerification() + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $this->factory->amount($actualAmount)->verify(); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) use ($expectedAmount) { + return $request->amount()->is($expectedAmount); + }); + } + + /** @test */ + public function can_not_assert_incorrect_fake_amount_in_currencies() + { + config([ + 'toman.currency' => 'toman', + ]); + + $this->factory->fakeVerification() + ->withTransactionId('tid_100') + ->withReferenceId('ref_100') + ->successful(); + + $this->factory->amount(10)->verify(); + + $this->expectException(AssertionFailedError::class); + + $this->factory->assertCheckedForVerification(function (PendingRequest $request) { + return $request->amount()->is(Money::Rial(10)); + }); + } +} diff --git a/tests/Gateways/Zarinpal/PendingRequestTest.php b/tests/Gateways/Zarinpal/PendingRequestTest.php new file mode 100644 index 0000000..8acd277 --- /dev/null +++ b/tests/Gateways/Zarinpal/PendingRequestTest.php @@ -0,0 +1,86 @@ +gateway = new Gateway(); + + $this->factory = new Factory($this->gateway); + } + + /** @test */ + public function can_set_merchant_id_elegantly() + { + // We need to ensure that developer can set merchant id elegantly, with their preferred methods + + $this->gateway->setConfig(['merchant_id' => 'AAA']); + self::assertEquals('AAA', $this->factory->merchantId()); + + self::assertEquals( + 'BBB', + $this->factory->merchantId('BBB')->merchantId() + ); + } + + /** @test */ + public function can_set_callback_url_elegantly() + { + $this->app['router']->get('/callback1')->name('payment.callback'); + + config(['toman.callback_route' => 'payment.callback']); + self::assertEquals(route('payment.callback'), $this->factory->callback()); + + self::assertEquals( + 'https://example.com', + $this->factory->callback('https://example.com')->callback() + ); + } + + /** @test */ + public function can_set_description_elegantly() + { + config(['toman.description' => 'Paying :amount for invoice 1']); + self::assertEquals( + 'Paying 5000 for invoice 1', + $this->factory->amount(5000)->description('Paying :amount for invoice 1')->description() + ); + + self::assertEquals( + 'Paying 5000 for invoice 2', + $this->factory->description('Paying :amount for invoice 2')->amount(5000)->description() + ); + } + + /** @test */ + public function can_set_mobile_elegantly() + { + self::assertEquals( + '09350000000', + $this->factory->mobile('09350000000')->mobile() + ); + } + + /** @test */ + public function can_set_email_elegantly() + { + self::assertEquals( + 'amirreza@exmaple.com', + $this->factory->email('amirreza@exmaple.com')->email() + ); + } +} diff --git a/tests/Gateways/Zarinpal/Provider.php b/tests/Gateways/Zarinpal/Provider.php new file mode 100644 index 0000000..4becac9 --- /dev/null +++ b/tests/Gateways/Zarinpal/Provider.php @@ -0,0 +1,117 @@ + [true, 'sandbox.zarinpal.com'], + 'Production' => [false, 'www.zarinpal.com'], + ]; + } + + public function clientErrorProvider() + { + return [ + [200, Status::INCOMPLETE_DATA, 'incomplete_data'], // 404 HTTP code is not guaranteed for errors + [200, -10, '-10'], // Yeah! It happens, and is not documented! + [404, Status::INCOMPLETE_DATA, 'incomplete_data'], + [404, Status::WRONG_IP_OR_MERCHANT_ID, 'wrong_ip_or_merchant_id'], + [404, Status::SHAPARAK_LIMITED, 'shaparak_limited'], + [404, Status::INSUFFICIENT_USER_LEVEL, 'insufficient_user_level'], + [404, Status::REQUEST_NOT_FOUND, 'request_not_found'], + [404, Status::UNABLE_TO_EDIT_REQUEST, 'unable_to_edit_request'], + [404, Status::NO_FINANCIAL_OPERATION, 'no_financial_operation'], + [404, Status::FAILED_TRANSACTION, 'failed_transaction'], + [404, Status::AMOUNTS_NOT_EQUAL, 'amounts_not_equal'], + [404, Status::TRANSACTION_SPLITTING_LIMITED, 'transaction_splitting_limited'], + [404, Status::METHOD_ACCESS_DENIED, 'method_access_denied'], + [404, Status::INVALID_ADDITIONAL_DATA, 'invalid_additional_data'], + [404, Status::INVALID_EXPIRATION_RANGE, 'invalid_expiration_range'], + [404, Status::REQUEST_ARCHIVED, 'request_archived'], + [404, Status::UNEXPECTED, 'unexpected'], + ]; + } + + public static function tomanBasedAmountProvider() + { + return [ + // [ + // config value, + // input amount + // expected amount value in Toman + // ], + 'Default currency is Toman (via config)' => [ + 'toman', + 5, + 5, + ], + 'Default currency is Rial (via config)' => [ + 'rial', + 50, + 5, + ], + 'Default currency is Toman (when no config is set)' => [ + null, + 5, + 5, + ], + 'Currency is Rial (overridden)' => [ + 'toman', + Money::Rial(50), + 5, + ], + 'Currency is Toman (overridden)' => [ + 'toman', + Money::Toman(50), + 50, + ], + ]; + } + + public static function fakeTomanBasedAmountProvider() + { + return [ + 'Default currency is Toman (via config)' => [ + 'toman', + 5, + Money::Toman(5), + ], + 'Default currency is Rial (via config)' => [ + 'rial', + 50, + Money::Toman(5), + ], + 'Default currency is Toman (when no config is set)' => [ + null, + 5, + Money::Toman(5), + ], + 'Currency is Rial (overridden) compared with Toman' => [ + 'toman', + Money::Rial(50), + Money::Toman(5), + ], + 'Currency is Toman (overridden) compared with Toman' => [ + 'toman', + Money::Toman(50), + Money::Toman(50), + ], + 'Currency is Toman (overridden) compared with Rial' => [ + 'toman', + Money::Toman(5), + Money::Rial(50), + ], + 'Currency is Rial (overridden) compared with Rial' => [ + 'toman', + Money::Rial(50), + Money::Rial(50), + ], + ]; + } +} diff --git a/tests/Gateways/Zarinpal/RequestTest.php b/tests/Gateways/Zarinpal/RequestTest.php new file mode 100644 index 0000000..0f23fcd --- /dev/null +++ b/tests/Gateways/Zarinpal/RequestTest.php @@ -0,0 +1,280 @@ +gateway = new Gateway(); + + $this->factory = new Factory($this->gateway); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::endpointProvider() + */ + public function can_request_payment(bool $sandbox, string $baseUrl) + { + // By sending a request to create a new ZarinPal transaction, + // we need to ensure that it sends request to proper endpoint, with correct + // data and amount. + // We also need to check that created payment can be redirected to default gateway + // or the specific one. + + config([ + 'toman.currency' => 'toman', + ]); + + Http::fake([ + "$baseUrl/pg/rest/WebGate/PaymentRequest.json" => Http::response([ + 'Status' => '100', + 'Authority' => 'A0000012345', + ], 200), + ]); + + $this->gateway->setConfig([ + 'sandbox' => $sandbox, + 'merchant_id' => 'xxxx-xxxx-xxxx-xxxx', + ]); + + $gateway = $this->factory + ->callback('https://example.com/callback') + ->amount(1500) + ->description('Payment for :amount.') + ->data('Mobile', '09350000000') + ->email('amirreza@example.com'); + + tap($gateway->request(), function (RequestedPayment $request) use ($baseUrl) { + Http::assertSent(function (Request $request) use ($baseUrl) { + return $request->method() === 'POST' + && $request->url() === "https://$baseUrl/pg/rest/WebGate/PaymentRequest.json" + && $request['MerchantID'] === 'xxxx-xxxx-xxxx-xxxx' + && $request['Amount'] == 1500 + && $request['CallbackURL'] === 'https://example.com/callback' + && $request['Description'] === 'Payment for 1500.' + && $request['Mobile'] === '09350000000' + && $request['Email'] === 'amirreza@example.com'; + }); + + // Since request is successful, we need to ensure that nothing can be + // thrown and determiners are correct + $request->throw(); + self::assertTrue($request->successful()); + self::assertFalse($request->failed()); + + self::assertEquals('A0000012345', $request->transactionId()); + + $redirectDefault = $request->pay(); + self::assertInstanceOf(RedirectResponse::class, $redirectDefault); + self::assertEquals("https://$baseUrl/pg/StartPay/A0000012345", $redirectDefault->getTargetUrl()); + self::assertEquals("https://$baseUrl/pg/StartPay/A0000012345", $request->paymentUrl()); + + $redirectSpecific = $request->pay(['gateway' => 'example']); + self::assertInstanceOf(RedirectResponse::class, $redirectSpecific); + self::assertEquals("https://$baseUrl/pg/StartPay/A0000012345/example", $redirectSpecific->getTargetUrl()); + self::assertEquals("https://$baseUrl/pg/StartPay/A0000012345/example", $request->paymentUrl(['gateway' => 'example'])); + }); + } + + /** @test */ + public function fails_with_server_error() + { + // When requesting a valid payment result in 5xx error, we consider this as an + // issue in gateway-side (server) and expect an expect API to provide proper results + + Http::fake([ + 'www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json' => Http::response(null, 555), + ]); + + tap($this->factory->request(), function (RequestedPayment $request) { + self::assertFalse($request->successful()); + self::assertTrue($request->failed()); + + try { + $request->throw(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + self::assertEquals(555, $exception->getCode()); + self::assertEquals($exception->getMessage(), $request->message()); + self::assertEquals($exception->getCode(), $request->status()); + } + + try { + $request->transactionId(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + } + + try { + $request->paymentUrl(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + } + + try { + $request->pay(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + } + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::clientErrorProvider() + */ + public function fails_with_client_error_without_message($httpStatus, $statusCode, $messageKey) + { + // When requesting a valid payment result in 4xx error, but there is nothing in errors data, + // we consider this as an issue in merchant-side (client) and expect an expect API to provide + // proper results. + + Http::fake([ + 'www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json' => Http::response([ + 'Status' => $statusCode, + ], $httpStatus), + ]); + + tap($this->factory->request(), function (RequestedPayment $request) use ($statusCode, $messageKey) { + self::assertFalse($request->successful()); + self::assertTrue($request->failed()); + + try { + $request->throw(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + self::assertEquals($statusCode, $exception->getCode()); + self::assertEquals($exception->getCode(), $request->status()); + + self::assertEquals(__("toman::zarinpal.status.$messageKey"), $exception->getMessage()); + self::assertEquals(__("toman::zarinpal.status.$messageKey"), $request->message()); + self::assertEquals([__("toman::zarinpal.status.$messageKey")], $request->messages()); + } + + try { + $request->transactionId(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + + try { + $request->paymentUrl(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + + try { + $request->pay(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::clientErrorProvider() + */ + public function fails_with_client_error_with_message($httpStatus, $statusCode, $messageKey) + { + // When requesting a valid payment result in 4xx error, with given error messages, + // we consider this as an issue in merchant-side (client) and expect an expect API + // to provide proper results + + Http::fake([ + 'www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json' => Http::response([ + 'Status' => $statusCode, + 'errors' => [ + 'Email' => [ + 'The email must be a valid email address.', + ], + 'Amount' => [ + 'The amount must be valid.', + ], + ], + ], $httpStatus), + ]); + + tap($this->factory->request(), function (RequestedPayment $request) use ($statusCode, $messageKey) { + self::assertFalse($request->successful()); + self::assertTrue($request->failed()); + + try { + $request->throw(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayClientException $exception) { + self::assertEquals($statusCode, $exception->getCode()); + self::assertEquals($exception->getCode(), $request->status()); + + self::assertEquals(__("toman::zarinpal.status.$messageKey"), $exception->getMessage()); + self::assertEquals('The email must be a valid email address.', $request->message()); + self::assertEquals([ + 'Email' => ['The email must be a valid email address.'], + 'Amount' => ['The amount must be valid.'], + ], $request->messages()); + } + + try { + $request->transactionId(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + + try { + $request->paymentUrl(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + + try { + $request->pay(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + } + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::tomanBasedAmountProvider() + */ + public function can_set_amount_in_different_currencies($configCurrency, $actualAmount, $expectedAmountValue) + { + config([ + 'toman.currency' => $configCurrency, + ]); + + Http::fake([ + 'www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json' => Http::response([ + 'Status' => '100', + 'Authority' => 'A0000012345', + ], 200), + ]); + + $this->factory->amount($actualAmount)->request(); + + Http::assertSent(function (Request $request) use ($expectedAmountValue) { + return $request['Amount'] == $expectedAmountValue; + }); + } +} diff --git a/tests/Gateways/Zarinpal/RequesterTest.php b/tests/Gateways/Zarinpal/RequesterTest.php deleted file mode 100644 index 7e7f3e0..0000000 --- a/tests/Gateways/Zarinpal/RequesterTest.php +++ /dev/null @@ -1,318 +0,0 @@ - 'xxxx-xxxx-xxxx-xxxx', - ], $this->mockedPurchaseResponse()); - - $gateway->callback('https://example.com/verify-here') - ->amount(1500) - ->description('An awesome payment gateway!') - ->data('Mobile', '09350000000') - ->email('amirreza@example.com'); - - $paymentRequest = $gateway->request(); - - $this->assertLastRequestedUrlEquals('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json'); - $this->assertLastRequestedDataEquals([ - "MerchantID" => "xxxx-xxxx-xxxx-xxxx", - "Amount" => 1500, - "CallbackURL" => 'https://example.com/verify-here', - "Description" => 'An awesome payment gateway!', - "Mobile" => '09350000000', - "Email" => 'amirreza@example.com', - ]); - - self::assertEquals('0000012345', $paymentRequest->getTransactionId()); - self::assertEquals("https://www.zarinpal.com/pg/StartPay/0000012345", $paymentRequest->getPaymentUrl()); - - $redirect = $paymentRequest->pay(); - - self::assertInstanceOf(RedirectResponse::class, $redirect); - self::assertEquals("https://www.zarinpal.com/pg/StartPay/0000012345", $redirect->getTargetUrl()); - } - - /** @test */ - public function can_request_sandbox_payment() - { - $gateway = Requester::make([ - 'sandbox' => true, - 'merchant_id' => 'xxxx-xxxx-xxxx-xxxx', - ], $this->mockedPurchaseResponse()); - - $gateway->callback('https://example.com/verify-here') - ->amount(1500) - ->description('An awesome payment gateway!') - ->data('Mobile', '09350000000') - ->email('amirreza@example.com'); - - $paymentRequest = $gateway->request(); - - $this->assertLastRequestedUrlEquals('https://sandbox.zarinpal.com/pg/rest/WebGate/PaymentRequest.json'); - $this->assertLastRequestedDataEquals([ - "MerchantID" => "xxxx-xxxx-xxxx-xxxx", - "Amount" => 1500, - "CallbackURL" => 'https://example.com/verify-here', - "Description" => 'An awesome payment gateway!', - "Mobile" => '09350000000', - "Email" => 'amirreza@example.com', - ]); - - self::assertEquals('0000012345', $paymentRequest->getTransactionId()); - self::assertEquals("https://sandbox.zarinpal.com/pg/StartPay/0000012345", $paymentRequest->getPaymentUrl()); - - $redirect = $paymentRequest->pay(); - - self::assertInstanceOf(RedirectResponse::class, $redirect); - self::assertEquals("https://sandbox.zarinpal.com/pg/StartPay/0000012345", $redirect->getTargetUrl()); - } - - /** - * @group external - * @test - */ - public function can_request_a_real_sandbox_payment() - { - $gateway = Requester::make([ - 'sandbox' => true, - 'merchant_id' => env('ZARINPAL_MERCHANT_ID'), - ], app(GuzzleClient::class)); - - $gateway->callback('https://example.com/verify-here') - ->amount(1500) - ->description('My description!') - ->data('Email', 'amirreza@example.com') - ->mobile('09350000000'); - - self::assertGreaterThanOrEqual(3, strlen($gateway->request()->getTransactionId())); - } - - /** @test */ - public function converts_validation_error_to_exception() - { - $client = $this->mockedGuzzleClient(new Response(404, [], json_encode([ - 'Status' => -11, - 'Authority' => '', - 'errors' => [ - 'CallbackURL' => 'The callback u r l field is required.', - 'Email' => 'The email must be a valid email address.', - ] - ]))); - - $this->expectException( GatewayException::class ); - $this->expectExceptionMessage('The callback u r l field is required.'); - - Requester::make($this->validConfig(), $client)->request(); - - $exception = $this->getExpectedException(); - - self::assertEquals(-11, $exception->getCode()); - self::assertStringContainsString('callback', $exception->getMessage()); - } - - /** - * @test - * @dataProvider errorProvider - */ - public function converts_other_gateway_errors_to_exception($passes, $httpCode, $status) - { - $client = $this->mockedGuzzleClient(new Response($httpCode, [], json_encode([ - 'Status' => $status, - 'Authority' => '', - ]))); - - try { - Requester::make($this->validConfig(), $client)->request(); - self::assertTrue($passes); - } catch (GatewayException $exception) { - self::assertEquals($status, $exception->getCode()); - self::assertEquals(Status::toMessage($status), $exception->getMessage()); - return true; - } - - if (!$passes) { - self::fail('No GatewayException is thrown!'); - } - } - - public function errorProvider() - { - return [ - [true, 200, Status::OPERATION_SUCCEED], - [false, 404, Status::INCOMPLETE_DATA], - [false, 200, Status::INCOMPLETE_DATA], // 404 HTTP code is not guaranteed in documents - [false, 404, Status::WRONG_IP_OR_MERCHANT_ID], - [false, 404, Status::SHAPARAK_LIMITED], - [false, 404, Status::INSUFFICIENT_USER_LEVEL], - [false, 404, Status::REQUEST_NOT_FOUND], - [false, 404, Status::UNABLE_TO_EDIT_REQUEST], - [false, 404, Status::NO_FINANCIAL_OPERATION], - [false, 404, Status::FAILED_TRANSACTION], - [false, 404, Status::AMOUNTS_NOT_EQUAL], - [false, 404, Status::TRANSACTION_SPLITTING_LIMITED], - [false, 404, Status::METHOD_ACCESS_DENIED], - [false, 404, Status::INVALID_ADDITIONAL_DATA], - [false, 404, Status::INVALID_EXPIRATION_RANGE], - [false, 404, Status::REQUEST_ARCHIVED], - [false, 404, Status::OPERATION_SUCCEED], - [false, 404, Status::UNEXPECTED], - ]; - } - - /** @test */ - public function can_set_merchant_id_elegantly() - { - $client = $this->mockedPurchaseResponse(2); - - Requester::make($this->validConfig(['merchant_id' => '1111-1111-1111-1111']), $client) - ->request(); - $this->assertDataInRequest('1111-1111-1111-1111', 'MerchantID'); - - Requester::make($this->validConfig(['merchant_id' => '1111-1111-1111-1111']), $client) - ->data('MerchantID', '2222-2222-2222-2222') - ->request(); - $this->assertDataInRequest('2222-2222-2222-2222', 'MerchantID'); - } - - /** @test */ - public function can_set_amount_elegantly() - { - $client = $this->mockedPurchaseResponse(2); - - Requester::make($this->validConfig(), $client)->amount(1000)->request(); - $this->assertDataInRequest(1000, 'Amount'); - - Requester::make($this->validConfig(), $client)->data('Amount', 3500)->request(); - $this->assertDataInRequest(3500, 'Amount'); - } - - /** @test */ - public function can_set_callback_url_elegantly() - { - $this->app['router']->get('/verify-payment')->name('payment.callback'); - - $client = $this->mockedPurchaseResponse(3); - - config(['toman.callback_route' => 'payment.callback']); - Requester::make($this->validConfig(), $client)->request(); - $this->assertDataInRequest(URL::route('payment.callback'), 'CallbackURL'); - - Requester::make($this->validConfig(), $client)->callback('https://example.com/callbackA')->request(); - $this->assertDataInRequest('https://example.com/callbackA', 'CallbackURL'); - - Requester::make($this->validConfig(), $client)->data('CallbackURL', 'https://example.com/callbackB')->request(); - $this->assertDataInRequest('https://example.com/callbackB', 'CallbackURL'); - } - - /** @test */ - public function can_set_description_elegantly() - { - $client = $this->mockedPurchaseResponse(4); - - config(['toman.description' => 'Paying :amount for invoice']); - Requester::make($this->validConfig(), $client)->amount(5000)->request(); - $this->assertDataInRequest('Paying 5000 for invoice', 'Description'); - - Requester::make($this->validConfig(), $client)->description('Some descriptions')->request(); - $this->assertDataInRequest('Some descriptions', 'Description'); - - Requester::make($this->validConfig(), $client)->data('Description', 'Other text')->request(); - $this->assertDataInRequest('Other text', 'Description'); - } - - /** @test */ - public function can_set_mobile_elegantly() - { - $client = $this->mockedPurchaseResponse(2); - - Requester::make($this->validConfig(), $client)->mobile('09350000000')->request(); - $this->assertDataInRequest('09350000000', 'Mobile'); - - Requester::make($this->validConfig(), $client)->data('Mobile', '09351111111')->request(); - $this->assertDataInRequest('09351111111', 'Mobile'); - } - - /** @test */ - public function can_set_email_elegantly() - { - $client = $this->mockedPurchaseResponse(2); - - Requester::make($this->validConfig(), $client)->email('amirreza@example.com')->request(); - $this->assertDataInRequest('amirreza@example.com', 'Email'); - - Requester::make($this->validConfig(), $client)->data('Email', 'alireza@example.com')->request(); - $this->assertDataInRequest('alireza@example.com', 'Email'); - } - - /** - * @test - * @dataProvider sandboxProvider - */ - public function validates_sandbox($passes, $sandbox) - { - $client = $this->mockedPurchaseResponse(); - - $gateway = Requester::make($this->validConfig([ - 'sandbox' => $sandbox - ]), $client); - - try { - $gateway->request(); - self::assertTrue($passes); - } catch (InvalidConfigException $exception) { - self::assertFalse($passes); - self::assertStringContainsString('sandbox', $exception->getMessage()); - } - } - - public function sandboxProvider() - { - return [ - "Passes without value" => [true, null], - "Passes with false" => [true, false], - "Passes with true" => [true, true], - "Fails with string" => [false, 'true'], - "Fails with array" => [false, ['true']], - ]; - } - - private function validConfig($overridden = []) - { - return array_merge([ - 'sandbox' => true, - 'merchant_id' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', - ], $overridden); - } - - private function mockedPurchaseResponse($times = 1) - { - $responses = []; - foreach (range(1, $times) as $i) { - $responses[] = new Response(200, [], json_encode([ - 'Status' => 100, - 'Authority' => '0000012345', - ])); - } - - return $this->mockedGuzzleClient($responses); - } -} diff --git a/tests/Gateways/Zarinpal/VerificationTest.php b/tests/Gateways/Zarinpal/VerificationTest.php new file mode 100644 index 0000000..749d809 --- /dev/null +++ b/tests/Gateways/Zarinpal/VerificationTest.php @@ -0,0 +1,267 @@ +gateway = new Gateway(); + + $this->factory = new Factory($this->gateway); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::endpointProvider() + */ + public function can_verify_manually(bool $sandbox, string $baseUrl) + { + Http::fake([ + "$baseUrl/pg/rest/WebGate/PaymentVerification.json" => Http::response([ + 'Status' => '100', + 'RefID' => '1000020000', + ], 200), + ]); + + $this->gateway->setConfig([ + 'sandbox' => $sandbox, + 'merchant_id' => 'xxxx-xxxx-xxxx-xxxx', + ]); + + tap( + $this->factory + ->amount(1500) + ->transactionId('A0000012345') + ->verify(), + function (CheckedPayment $request) use ($baseUrl) { + Http::assertSent(function (Request $request) use ($baseUrl) { + return $request->method() === 'POST' + && $request->url() === "https://$baseUrl/pg/rest/WebGate/PaymentVerification.json" + && $request['MerchantID'] === 'xxxx-xxxx-xxxx-xxxx' + && $request['Amount'] == 1500 + && $request['Authority'] === 'A0000012345'; + }); + + // Since request is successful, we need to ensure that nothing can be + // thrown and determiners are correct + $request->throw(); + self::assertTrue($request->successful()); + self::assertFalse($request->alreadyVerified()); + self::assertFalse($request->failed()); + + self::assertEquals('A0000012345', $request->transactionId()); + self::assertEquals('1000020000', $request->referenceId()); + } + ); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::endpointProvider() + */ + public function can_verify_callback_request(bool $sandbox, string $baseUrl) + { + Http::fake([ + "$baseUrl/pg/rest/WebGate/PaymentVerification.json" => Http::response([ + 'Status' => '100', + 'RefID' => '1000020000', + ], 200), + ]); + + request()->merge([ + 'Authority' => 'A0000012345', + ]); + + $this->gateway->setConfig([ + 'sandbox' => $sandbox, + 'merchant_id' => 'xxxx-xxxx-xxxx-xxxx', + ]); + + $gateway = $this->factory->inspectCallbackRequest()->amount(1500); + + tap($gateway->verify(), function (CheckedPayment $request) use ($baseUrl) { + Http::assertSent(function (Request $request) use ($baseUrl) { + return $request->method() === 'POST' + && $request->url() === "https://$baseUrl/pg/rest/WebGate/PaymentVerification.json" + && $request['MerchantID'] === 'xxxx-xxxx-xxxx-xxxx' + && $request['Amount'] == 1500 + && $request['Authority'] === 'A0000012345'; + }); + + $request->throw(); + self::assertTrue($request->successful()); + self::assertFalse($request->alreadyVerified()); + self::assertFalse($request->failed()); + + self::assertEquals('A0000012345', $request->transactionId()); + self::assertEquals('1000020000', $request->referenceId()); + }); + } + + public static function badTransactionId() + { + return [ + 'Empty' => [''], + 'Array' => [['A0000012345']], + ]; + } + + /** + * @test + * @dataProvider badTransactionId + */ + public function validates_callback_transaction_id($value) + { + Http::fake(); + + request()->merge([ + 'Authority' => $value, + ]); + + $this->expectException(ValidationException::class); + + $this->factory->inspectCallbackRequest(); + } + + /** @test */ + public function can_determine_if_transaction_has_already_been_verified() + { + Http::fake([ + 'www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' => Http::response([ + 'Status' => '101', + 'RefID' => '1000020000', + ], 200), + ]); + + $gateway = $this->factory + ->amount(1500) + ->transactionId('A0000012345'); + + tap($gateway->verify(), function (CheckedPayment $request) { + $request->throw(); + self::assertFalse($request->successful()); + self::assertTrue($request->alreadyVerified()); + self::assertFalse($request->failed()); + + self::assertEquals('A0000012345', $request->transactionId()); + self::assertEquals('1000020000', $request->referenceId()); + }); + } + + /** @test */ + public function fails_with_server_error() + { + Http::fake([ + 'www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' => Http::response(null, 555), + ]); + + $gateway = $this->factory->transactionId('A0000012345'); + + tap($gateway->verify(), function (CheckedPayment $verification) { + self::assertFalse($verification->successful()); + self::assertFalse($verification->alreadyVerified()); + self::assertTrue($verification->failed()); + + try { + $verification->throw(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + self::assertEquals(555, $exception->getCode()); + self::assertEquals($exception->getMessage(), $verification->message()); + self::assertEquals($exception->getCode(), $verification->status()); + } + + self::assertNotEmpty($verification->transactionId()); + + try { + $verification->referenceId(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayServerException $exception) { + } + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::clientErrorProvider() + */ + public function fails_with_client_error($httpStatus, $statusCode, $messageKey) + { + Http::fake([ + 'www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' => Http::response([ + 'Status' => $statusCode, + ], $httpStatus), + ]); + + $gateway = $this->factory->transactionId('A0000012345'); + + tap($gateway->verify(), function (CheckedPayment $verification) use ($statusCode, $messageKey) { + self::assertFalse($verification->successful()); + self::assertFalse($verification->alreadyVerified()); + self::assertTrue($verification->failed()); + + try { + $verification->throw(); + $this->fail('GatewayClientException has no thrown.'); + } catch (GatewayClientException $exception) { + self::assertEquals($statusCode, $exception->getCode()); + self::assertEquals($exception->getCode(), $verification->status()); + + self::assertEquals(__("toman::zarinpal.status.$messageKey"), $exception->getMessage()); + self::assertEquals(__("toman::zarinpal.status.$messageKey"), $verification->message()); + self::assertEquals([__("toman::zarinpal.status.$messageKey")], $verification->messages()); + } + + self::assertNotEmpty($verification->transactionId()); + + try { + $verification->referenceId(); + $this->fail('GatewayServerException has no thrown.'); + } catch (GatewayClientException $exception) { + } + }); + } + + /** + * @test + * @dataProvider \Evryn\LaravelToman\Tests\Gateways\Zarinpal\Provider::tomanBasedAmountProvider() + */ + public function can_set_amount_in_different_currencies($configCurrency, $actualAmount, $expectedAmountValue) + { + config([ + 'toman.currency' => $configCurrency, + ]); + + Http::fake([ + 'www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json' => Http::response([ + 'Status' => '100', + 'RefID' => '1000020000', + ], 200), + ]); + + $this->factory->amount($actualAmount)->verify(); + + Http::assertSent(function (Request $request) use ($expectedAmountValue) { + return $request['Amount'] == $expectedAmountValue; + }); + } +} diff --git a/tests/Gateways/Zarinpal/VerifierTest.php b/tests/Gateways/Zarinpal/VerifierTest.php deleted file mode 100644 index e0c69dc..0000000 --- a/tests/Gateways/Zarinpal/VerifierTest.php +++ /dev/null @@ -1,258 +0,0 @@ -withoutExceptionHandling(); - - $client = $this->mockedVerificationResponse(100, '77777777'); - - $request = Request::create('/callback', 'GET', [ - 'Status' => 'OK', - 'Authority' => '000000000000000000000000000000001234', - ]); - - $verifiedPayment = Verifier::make([ - 'merchant_id' => '1111-1111-1111-1111' - ], $client) - ->amount(1500) - ->verify($request); - - $this->assertLastRequestedUrlEquals('https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'); - $this->assertLastRequestedDataEquals([ - 'MerchantID' => '1111-1111-1111-1111', - 'Amount' => '1500', - 'Authority' => '000000000000000000000000000000001234', - ]); - self::assertEquals('77777777', $verifiedPayment->getReferenceId()); - } - - /** @test */ - public function verifies_incoming_sandbox_callback_correctly() - { - $this->withoutExceptionHandling(); - - $client = $this->mockedVerificationResponse(100, '77777777'); - - $request = Request::create('/callback', 'GET', [ - 'Status' => 'OK', - 'Authority' => '000000000000000000000000000000001234', - ]); - - $verifiedPayment = Verifier::make([ - 'sandbox' => true, - 'merchant_id' => '1111-1111-1111-1111' - ], $client) - ->amount(1500) - ->verify($request); - - $this->assertLastRequestedUrlEquals('https://sandbox.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'); - $this->assertLastRequestedDataEquals([ - 'MerchantID' => '1111-1111-1111-1111', - 'Amount' => '1500', - 'Authority' => '000000000000000000000000000000001234', - ]); - self::assertEquals('77777777', $verifiedPayment->getReferenceId()); - } - - /** @test */ - public function fails_without_verifying_when_status_is_not_ok() - { - $this->withoutExceptionHandling(); - $client = $this->mockedGuzzleClient(null); - - $request = Request::create('/callback', 'GET', [ - 'Status' => 'NOK', - 'Authority' => '000000000000000000000000000000001234', - ]); - - $this->expectException(GatewayException::class); - $this->expectExceptionMessage(Status::toMessage(Status::NOT_PAID)); - $this->expectExceptionCode(Status::NOT_PAID); - - Verifier::make($this->validConfig(), $client) - ->amount(5000) - ->verify($request); - } - - /** @test */ - public function converts_gateway_validation_error_to_exception() - { - $client = $this->mockedGuzzleClient(new Response(404, [], json_encode([ - 'Status' => -11, - 'Authority' => '', - 'errors' => [ - 'Authority' => 'The authority field is required.', - 'Amount' => 'The amount field is required.', - ] - ]))); - - $request = Request::create('/callback', 'GET', [ - 'Status' => 'OK', - 'Authority' => '000000000000000000000000000000001234', - ]); - - $this->expectException(GatewayException::class); - $this->expectExceptionMessage('The authority field is required.'); - $this->expectExceptionCode(-11); - - Verifier::make($this->validConfig(), $client) - ->amount(5000) - ->verify($request); - } - - /** - * @test - * @dataProvider errorProvider - */ - public function converts_validation_error_to_exception($passes, $httpCode, $status) - { - $client = $this->mockedGuzzleClient(new Response($httpCode, [], json_encode([ - 'Status' => $status, - 'RefID' => $status === 100 ? '1234' : 0 - ]))); - - $request = Request::create('/callback', 'GET', [ - 'Status' => 'OK', - 'Authority' => '000000000000000000000000000000001234', - ]); - - try { - Verifier::make($this->validConfig(), $client) - ->amount(5000) - ->verify($request); - self::assertTrue($passes); - } catch (GatewayException $exception) { - self::assertEquals($status, $exception->getCode()); - self::assertEquals(Status::toMessage($status), $exception->getMessage()); - return true; - } - - if (!$passes) - self::fail("Didn't thrown expected exception."); - } - - public function errorProvider() - { - return [ - [true, 200, Status::OPERATION_SUCCEED], - [false, 404, Status::INCOMPLETE_DATA], - [false, 200, Status::INCOMPLETE_DATA], // 404 HTTP code is not guaranteed in documents - [false, 404, Status::WRONG_IP_OR_MERCHANT_ID], - [false, 404, Status::SHAPARAK_LIMITED], - [false, 404, Status::INSUFFICIENT_USER_LEVEL], - [false, 404, Status::REQUEST_NOT_FOUND], - [false, 404, Status::UNABLE_TO_EDIT_REQUEST], - [false, 404, Status::NO_FINANCIAL_OPERATION], - [false, 404, Status::FAILED_TRANSACTION], - [false, 404, Status::AMOUNTS_NOT_EQUAL], - [false, 404, Status::TRANSACTION_SPLITTING_LIMITED], - [false, 404, Status::METHOD_ACCESS_DENIED], - [false, 404, Status::INVALID_ADDITIONAL_DATA], - [false, 404, Status::INVALID_EXPIRATION_RANGE], - [false, 404, Status::REQUEST_ARCHIVED], - [false, 404, Status::OPERATION_SUCCEED], - [false, 404, Status::UNEXPECTED], - ]; - } - - /** @test */ - public function can_set_amount_elegantly() - { - $this->withoutExceptionHandling(); - $client = $this->mockedVerificationResponse(100, '123', 2); - - $request = Request::create('/callback', 'GET', [ - 'Status' => 'OK', - 'Authority' => '000000000000000000000000000000001234', - ]); - - Verifier::make($this->validConfig(), $client) - ->amount(1000) - ->verify($request); - $this->assertDataInRequest(1000, 'Amount'); - - Verifier::make($this->validConfig(), $client) - ->data('Amount', 2500) - ->verify($request); - $this->assertDataInRequest(2500, 'Amount'); - } - - /** - * @test - * @dataProvider sandboxProvider - */ - public function validates_sandbox($passes, $sandbox) - { - $request = Request::create('/callback', 'GET', [ - 'Status' => 'OK', - 'Authority' => '000000000000000000000000000000001234', - ]); - - $client = $this->mockedVerificationResponse(100); - - $gateway = Verifier::make($this->validConfig([ - 'sandbox' => $sandbox - ]), $client); - - try { - $gateway->verify($request); - self::assertTrue($passes); - } catch (InvalidConfigException $exception) { - self::assertFalse($passes); - self::assertStringContainsString('sandbox', $exception->getMessage()); - } - } - - public function sandboxProvider() - { - return [ - "Passes without value" => [true, null], - "Passes with false" => [true, false], - "Passes with true" => [true, true], - "Fails with string" => [false, 'true'], - "Fails with array" => [false, ['true']], - ]; - } - - private function validConfig($overridden = []) - { - return array_merge([ - 'sandbox' => true, - 'merchant_id' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', - ], $overridden); - } - - private function mockedVerificationResponse($status, $ref_id = 0, $times = 1) - { - $responses = []; - foreach (range(1, $times) as $i) { - $responses[] = new Response(200, [], json_encode([ - 'Status' => $status, - 'RefID' => $ref_id, - ])); - } - - return $this->mockedGuzzleClient($responses); - } -} diff --git a/tests/LaravelTomanServiceProviderTest.php b/tests/LaravelTomanServiceProviderTest.php index c779dc3..88114c4 100644 --- a/tests/LaravelTomanServiceProviderTest.php +++ b/tests/LaravelTomanServiceProviderTest.php @@ -1,9 +1,7 @@ artisan('vendor:publish', [ '--provider' => 'Evryn\LaravelToman\LaravelTomanServiceProvider', - '--tag' => 'config' + '--tag' => 'config', ]); $this->assertFileExists($dest); @@ -29,9 +29,11 @@ public function publishes_config_correctly() /** @test */ public function publishes_translations_correctly() { + // We need to ensure that artisan can publish default translations files properly + $map = [ - __DIR__ . '/../resources/lang/en/zarinpal.php' => resource_path('lang/vendor/toman/en/zarinpal.php'), - __DIR__ . '/../resources/lang/fa/zarinpal.php' => resource_path('lang/vendor/toman/fa/zarinpal.php'), + __DIR__.'/../resources/lang/en/zarinpal.php' => resource_path('lang/vendor/toman/en/zarinpal.php'), + __DIR__.'/../resources/lang/fa/zarinpal.php' => resource_path('lang/vendor/toman/fa/zarinpal.php'), ]; foreach (array_values($map) as $dest) { @@ -40,7 +42,7 @@ public function publishes_translations_correctly() $this->artisan('vendor:publish', [ '--provider' => 'Evryn\LaravelToman\LaravelTomanServiceProvider', - '--tag' => 'lang' + '--tag' => 'lang', ]); foreach ($map as $source => $dest) { diff --git a/tests/Managers/PaymentRequestManagerTest.php b/tests/Managers/PaymentRequestManagerTest.php deleted file mode 100644 index 0c5f8a7..0000000 --- a/tests/Managers/PaymentRequestManagerTest.php +++ /dev/null @@ -1,47 +0,0 @@ -manager = new PaymentRequestManager($this->app); - } - - /** @test */ - public function gets_default_driver_from_config() - { - config(['toman.default' => 'foo']); - self::assertEquals('foo', $this->manager->getDefaultDriver()); - - config(['toman.default' => 'bar']); - self::assertEquals('bar', $this->manager->getDefaultDriver()); - } - - /** @test */ - public function creates_configured_zarinpal_drive() - { - $config = [ - 'sandbox' => true, - 'merchant_id' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', - ]; - - config(['toman.gateways.zarinpal' => $config]); - - $gateway = $this->manager->driver('zarinpal'); - - self::assertInstanceOf(Requester::class, $gateway); - self::assertEquals($config, $gateway->getConfig()); - } - - -} diff --git a/tests/Managers/PaymentVerifierManagerTest.php b/tests/Managers/PaymentVerifierManagerTest.php deleted file mode 100644 index 36d6a0f..0000000 --- a/tests/Managers/PaymentVerifierManagerTest.php +++ /dev/null @@ -1,48 +0,0 @@ -manager = new PaymentVerificationManager($this->app); - } - - /** @test */ - public function gets_default_driver_from_config() - { - config(['toman.default' => 'foo']); - self::assertEquals('foo', $this->manager->getDefaultDriver()); - - config(['toman.default' => 'bar']); - self::assertEquals('bar', $this->manager->getDefaultDriver()); - } - - /** @test */ - public function creates_configured_zarinpal_drive() - { - $config = [ - 'sandbox' => true, - 'merchant_id' => 'xxxxxxxx-yyyy-zzzz-wwww-xxxxxxxxxxxx', - ]; - - config(['toman.gateways.zarinpal' => $config]); - - $gateway = $this->manager->driver('zarinpal'); - - self::assertInstanceOf(Verifier::class, $gateway); - self::assertEquals($config, $gateway->getConfig()); - } - - -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 1b6f923..51a4955 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,17 +2,12 @@ namespace Evryn\LaravelToman\Tests; +use Evryn\LaravelToman\LaravelTomanServiceProvider; + abstract class TestCase extends \Orchestra\Testbench\TestCase { protected function getPackageProviders($app) { - return ['Evryn\LaravelToman\LaravelTomanServiceProvider']; - } - - protected function getPackageAliases($app) - { - return [ - 'Payment' => 'Evryn\LaravelToman\Facades\PaymentRequest' - ]; + return [LaravelTomanServiceProvider::class]; } }