├── src
├── Exception.php
├── Exceptions
│ ├── RefundException.php
│ ├── SettlementException.php
│ ├── RequestTokenException.php
│ └── VerificationException.php
├── Provider
│ ├── Exception.php
│ ├── ZarinpalProvider.php
│ ├── MelliProvider.php
│ ├── SaderatProvider.php
│ ├── SamanProvider.php
│ ├── AbstractProvider.php
│ ├── ParsianProvider.php
│ ├── PasargadProvider.php
│ ├── MellatProvider.php
│ └── AsanPardakhtProvider.php
├── Helper
│ └── Pasargad
│ │ ├── RSAKeyType.php
│ │ ├── RSAProcessor.php
│ │ └── RSA.php
├── Contracts
│ ├── Factory.php
│ ├── Provider.php
│ └── Transaction.php
├── Enums
│ └── ProviderName.php
├── Facades
│ └── Shaparak.php
├── ShaparakServiceProvider.php
└── ShaparakManager.php
├── .gitignore
├── views
└── goto-gate-form.blade.php
├── translations
├── en
│ └── shaparak.php
└── fa
│ └── shaparak.php
├── LICENSE
├── composer.json
├── config
└── shaparak.php
└── README.md
/src/Exception.php:
--------------------------------------------------------------------------------
1 |
2 | @foreach($parameters as $field => $value)
3 | @if(!is_null($value))
4 |
5 | @endif
6 | @endforeach
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | @if($autoSubmit === true)
16 |
20 | @endif
21 |
22 |
23 |
--------------------------------------------------------------------------------
/translations/en/shaparak.php:
--------------------------------------------------------------------------------
1 | 'canceled_by_user',
5 | 'could_not_request_token' => 'could_not_request_token',
6 | 'could_not_verify_transaction' => 'could_not_verify_transaction',
7 | 'could_not_inquiry_payment' => 'could_not_inquiry_payment',
8 | 'could_not_settle_payment' => 'could_not_settle_payment',
9 | 'could_not_refund_payment' => 'could_not_refund_payment',
10 | 'gate_not_ready' => 'gate_not_ready',
11 | 'goto_gate' => 'goto_gate',
12 | 'invalid_response' => 'invalid_response',
13 | 'token_failed' => 'token_failed',
14 | 'verify_failed' => 'verify_failed',
15 | 'inquiry_failed' => 'inquiry_failed',
16 | 'settle_failed' => 'settle_failed',
17 | 'refund_failed' => 'refund_failed',
18 | ];
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # Shaparak Component v1.0 for Laravel 5+
2 |
3 | ## Laravel
4 |
5 |
6 | The MIT License (MIT)
7 |
8 | Copyright (c) 2016 Aboozar Ghaffari
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a copy
11 | of this software and associated documentation files (the "Software"), to deal
12 | in the Software without restriction, including without limitation the rights
13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | copies of the Software, and to permit persons to whom the Software is
15 | furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in all
18 | copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | SOFTWARE.
27 |
--------------------------------------------------------------------------------
/translations/fa/shaparak.php:
--------------------------------------------------------------------------------
1 | 'تراکنش بانکی توسط کاربر لغو شد',
5 | 'could_not_request_token' => 'امکان درخواست توکن برای این تراکنش وجود ندارد',
6 | 'could_not_verify_transaction' => 'امکان تایید این تراکنش وجود ندارد',
7 | 'could_not_inquiry_payment' => 'امکان استعلام این تراکنش وجود ندارد',
8 | 'could_not_settle_payment' => 'امکان تسویه برای این تراکنش وجود ندارد',
9 | 'could_not_refund_payment' => 'امکان برگشت زدن این تراکنش وجود ندارد',
10 | 'could_not_pass_abuse_checklist' => 'چک لیست امنیتی پاس نشد!',
11 | 'refund_payment_already_exist' => 'تراکنش قبلا برگشت خورده است',
12 | 'gate_not_ready' => 'درگاه پرداختی یا وجود ندارد یا آماده سرویس دهی نمی باشد',
13 | 'gateway_tokens_are_not_same' => 'توکن درگاه با تراکنش برابر نیست!',
14 | 'goto_gate' => 'انتقال به درگاه پرداخت',
15 | 'invalid_response' => 'پاسخ معتبر از سرور درگاه پرداخت دریافت نشد!',
16 | 'token_failed' => 'درخواست توکن تراکنش با موفقیت انجام نشد!',
17 | 'verify_failed' => 'تایید تراکنش با موفقیت انجام نشد!',
18 | 'inquiry_failed' => 'بررسی وضعیت تراکنش با موفقیت انجام نشد!',
19 | 'settle_failed' => 'تایید بعد از تراکنش با موفقیت انجام نشد!',
20 | 'refund_failed' => 'بازگشت تراکنش با موفقیت انجام نشد!',
21 | 'amounts_not_match' => 'مقدار فاکتور با مقدار برگشتی از درگاه برابر نیست!',
22 | ];
23 |
--------------------------------------------------------------------------------
/src/ShaparakServiceProvider.php:
--------------------------------------------------------------------------------
1 | registerResources();
18 | $this->registerPublishing();
19 | }
20 |
21 | /**
22 | * Get the services provided by the provider.
23 | *
24 | * @return void
25 | */
26 | public function register()
27 | {
28 | $this->app->singleton(Factory::class, function ($app) {
29 | return new ShaparakManager($app);
30 | });
31 | }
32 |
33 | /**
34 | * Get the services provided by the provider.
35 | *
36 | * @return array
37 | */
38 | public function provides()
39 | {
40 | return [Factory::class];
41 | }
42 |
43 | /**
44 | * Determine if the provider is deferred.
45 | *
46 | * @return bool
47 | */
48 | public function isDeferred()
49 | {
50 | return true;
51 | }
52 |
53 | protected function registerResources()
54 | {
55 | $this->loadViewsFrom(__DIR__.'/../views/', 'shaparak');
56 |
57 | $this->publishes([
58 | __DIR__.'/../translations/' => base_path('/lang/vendor/shaparak'),
59 | ], 'translations');
60 |
61 | $this->loadTranslationsFrom(__DIR__.'/../translations', 'shaparak');
62 | }
63 |
64 | protected function registerPublishing()
65 | {
66 | $this->publishes([
67 | __DIR__.'/../views/' => resource_path('/views/vendor/shaparak'),
68 | ], 'views');
69 |
70 | $this->publishes([
71 | __DIR__.'/../config/shaparak.php' => config_path('shaparak.php'),
72 | ], 'config');
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "php-monsters/shaparak",
3 | "description": "Iranian payment gateways handler for laravel based applications",
4 | "keywords": [
5 | "payment",
6 | "shaparak",
7 | "shetab",
8 | "asanpay",
9 | "bank",
10 | "online payment",
11 | "gateway",
12 | "iran"
13 | ],
14 | "type": "library",
15 | "license": "MIT",
16 | "authors": [
17 | {
18 | "name": "Aboozar Ghaffari ",
19 | "email": "aboozar.ghf@gmail.com"
20 | },
21 | {
22 | "name": "Milad Kianmehr",
23 | "email": "milad.kian@gmail.com"
24 | },
25 | {
26 | "name": "Maryam Nabiyan",
27 | "email": "maryam.nbyn@gmail.com"
28 | }
29 | ],
30 | "require": {
31 | "php": ">8.0",
32 | "ext-curl": "*",
33 | "ext-json": "*",
34 | "ext-openssl": "*",
35 | "ext-simplexml": "*",
36 | "ext-soap": "*",
37 | "ext-xml": "*",
38 | "ext-bcmath": "*",
39 | "illuminate/support": ">=8.0",
40 | "illuminate/view": ">=8.0",
41 | "php-monsters/laravel-xlog": "^1.3"
42 | },
43 | "require-dev": {
44 | "mockery/mockery": "^1.4.2",
45 | "phpunit/phpunit": "^9.5"
46 | },
47 | "autoload-dev": {
48 | "psr-4": {
49 | "PhpMonsters\\Shaparak\\Tests\\": "tests/"
50 | }
51 | },
52 | "autoload": {
53 | "psr-4": {
54 | "PhpMonsters\\Shaparak\\": "src/"
55 | }
56 | },
57 | "extra": {
58 | "laravel": {
59 | "providers": [
60 | "PhpMonsters\\Shaparak\\ShaparakServiceProvider"
61 | ],
62 | "aliases": {
63 | "Shaparak": "PhpMonsters\\Shaparak\\Facades\\Shaparak"
64 | }
65 | }
66 | },
67 | "config": {
68 | "sort-packages": true
69 | },
70 | "minimum-stability": "stable"
71 | }
72 |
--------------------------------------------------------------------------------
/src/Helper/Pasargad/RSAProcessor.php:
--------------------------------------------------------------------------------
1 | modulus = RSA::binary_to_number(base64_decode($xmlObj->Modulus));
36 | $this->public_key = RSA::binary_to_number(base64_decode($xmlObj->Exponent));
37 | $this->private_key = RSA::binary_to_number(base64_decode($xmlObj->D));
38 | $this->key_length = strlen(base64_decode($xmlObj->Modulus)) * 8;
39 | }
40 |
41 | public function getPublicKey()
42 | {
43 | return $this->public_key;
44 | }
45 |
46 | public function getPrivateKey()
47 | {
48 | return $this->private_key;
49 | }
50 |
51 | public function getKeyLength(): int
52 | {
53 | return $this->key_length;
54 | }
55 |
56 | public function getModulus()
57 | {
58 | return $this->modulus;
59 | }
60 |
61 | public function encrypt($data): string
62 | {
63 | return base64_encode(RSA::rsa_encrypt($data, $this->public_key, $this->modulus, $this->key_length));
64 | }
65 |
66 | public function decrypt($data)
67 | {
68 | return RSA::rsa_decrypt($data, $this->private_key, $this->modulus, $this->key_length);
69 | }
70 |
71 | public function sign($data): string
72 | {
73 | return RSA::rsa_sign($data, $this->private_key, $this->modulus, $this->key_length);
74 | }
75 |
76 | public function verify($data)
77 | {
78 | return RSA::rsa_verify($data, $this->public_key, $this->modulus, $this->key_length);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Contracts/Provider.php:
--------------------------------------------------------------------------------
1 | = 0; $i--) {
132 | $digit = ord($data[$i]);
133 | $part_res = bcmul($digit, $radix);
134 | $result = bcadd($result, $part_res);
135 | $radix = bcmul($radix, $base);
136 | }
137 |
138 | return $result;
139 | }
140 |
141 | public static function number_to_binary($number, $blocksize)
142 | {
143 | $base = '256';
144 | $result = '';
145 | $div = $number;
146 | while ($div > 0) {
147 | $mod = bcmod($div, $base);
148 | $div = bcdiv($div, $base);
149 | $result = chr($mod).$result;
150 | }
151 |
152 | return str_pad($result, $blocksize, "\x00", STR_PAD_LEFT);
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/config/shaparak.php:
--------------------------------------------------------------------------------
1 | env('SHAPARAK_MODE', 'production'),
18 |
19 | 'banktest_base_url' => 'https://sandbox.banktest.ir',
20 |
21 | 'log' => [
22 | 'prefix' => 'SHAPARAK->',
23 | ],
24 |
25 | 'iban' => env('SETTLEMENT_IBAN'),
26 |
27 | 'providers' => [
28 | /*
29 | |--------------------------------------------------------------------------
30 | | AsanPardakht gateway configuration
31 | |--------------------------------------------------------------------------
32 | */
33 | 'asanpardakht' => [
34 | 'username' => env('AP_USERNAME'),
35 | 'password' => env('AP_PASSWORD'),
36 | 'terminal_id' => env('AP_MERCHANT_ID'),
37 | 'key' => env('AP_KEY'),
38 | 'iv' => env('AP_IV'),
39 | 'cumulative' => env('AP_CUMULATIVE', false),
40 | ],
41 | /*
42 | |--------------------------------------------------------------------------
43 | | Saman gateway configuration
44 | |--------------------------------------------------------------------------
45 | */
46 | 'saman' => [
47 | 'terminal_id' => env('SAMAN_TERMINAL_ID'),
48 | 'terminal_pass' => env('SAMAN_TERMINAL_PASS'),
49 | 'cumulative' => env('SAMAN_CUMULATIVE', false),
50 | ],
51 | /*
52 | |--------------------------------------------------------------------------
53 | | Parsian gateway configuration
54 | |--------------------------------------------------------------------------
55 | */
56 | 'parsian' => [
57 | 'pin' => env('PARSIAN_PIN', ''),
58 | 'cumulative' => env('PARSIAN_CUMULATIVE', false),
59 | ],
60 | /*
61 | |--------------------------------------------------------------------------
62 | | Pasargad gateway configuration
63 | |--------------------------------------------------------------------------
64 | */
65 | 'pasargad' => [
66 | 'terminal_id' => env('PASARGAD_TERMINAL_ID'),
67 | 'merchant_id' => env('PASARGAD_MERCHANT_ID'),
68 | 'certificate_path' => env('PASARGAD_CERT_PATH', storage_path('shaparak/pasargad/certificate.xml')),
69 | 'cumulative' => env('PASARGAD_CUMULATIVE', false),
70 | ],
71 | /*
72 | |--------------------------------------------------------------------------
73 | | Mellat gateway configuration
74 | |--------------------------------------------------------------------------
75 | */
76 | 'mellat' => [
77 | 'username' => env('MELLAT_USERNAME'),
78 | 'password' => env('MELLAT_PASSWORD'),
79 | 'terminal_id' => env('MELLAT_TERMINAL_ID'),
80 | 'cumulative' => env('MELLAT_CUMULATIVE', false),
81 | ],
82 | /*
83 | |--------------------------------------------------------------------------
84 | | Melli/Sadad gateway configuration
85 | |--------------------------------------------------------------------------
86 | */
87 | 'melli' => [
88 | 'merchant_id' => env('MELLI_MERCHANT_ID'),
89 | 'terminal_id' => env('MELLI_TERMINAL_ID'),
90 | 'transaction_key' => env('MELLI_TRANS_KEY'),
91 | 'cumulative' => env('MELLI_CUMULATIVE', false),
92 | ],
93 | /*
94 | |--------------------------------------------------------------------------
95 | | Zarinpal gateway configuration
96 | |--------------------------------------------------------------------------
97 | */
98 | 'saderat' => [
99 | 'terminal_id' => env('SADERAT_MERCHANT_ID'),
100 | 'cumulative' => env('SADERAT_CUMULATIVE', false),
101 | ],
102 | /*
103 | |--------------------------------------------------------------------------
104 | | Zarinpal gateway configuration
105 | |--------------------------------------------------------------------------
106 | */
107 | 'zarinpal' => [
108 | 'merchant_id' => env('ZARINPAL_MERCHANT_ID'),
109 | 'cumulative' => env('ZARINPAL_CUMULATIVE', false),
110 | ],
111 | ],
112 |
113 | /*
114 | |--------------------------------------------------------------------------
115 | | httpClient Options including soapClient/Guzzle/Curl
116 | |--------------------------------------------------------------------------
117 | |
118 | | options: Options
119 | |
120 | */
121 | 'httpClientOptions' => [
122 | 'soap' => [],
123 | 'curl' => [],
124 | ],
125 | ];
126 |
--------------------------------------------------------------------------------
/src/ShaparakManager.php:
--------------------------------------------------------------------------------
1 | '.$message;
35 |
36 | forward_static_call(['PhpMonsters\Log\Facades\XLog', $level], $message, $params);
37 | }
38 |
39 | /**
40 | * Get a driver instance.
41 | *
42 | * @param string $driver driver name
43 | * @param array $config runtime configuration for the driver instead of reading from config file
44 | * @return mixed
45 | */
46 | public function with(string $driver, Transaction $transaction, array $config = [])
47 | {
48 | $this->transaction = $transaction;
49 |
50 | if (! empty($config)) {
51 | $this->runtimeConfig = $config;
52 | }
53 |
54 | return $this->driver($driver);
55 | }
56 |
57 | /**
58 | * Get the default driver name.
59 | *
60 | * @return string
61 | *
62 | * @throws InvalidArgumentException
63 | */
64 | public function getDefaultDriver()
65 | {
66 | throw new InvalidArgumentException('No Shaparak driver was specified.');
67 | }
68 |
69 | /**
70 | * Build a Shaparak provider instance.
71 | */
72 | public function buildProvider(string $provider, array $config): Provider
73 | {
74 | return new $provider(
75 | $this->transaction,
76 | $config,
77 | Arr::get($config, 'mode', config('shaparak.mode', 'production')),
78 | Arr::get($config, 'httpClientOptions', [])
79 | );
80 | }
81 |
82 | /**
83 | * get provider configuration runtime array or config based configuration
84 | */
85 | protected function getConfig(ProviderName $driver): array
86 | {
87 | if (empty($this->runtimeConfig)) {
88 | return $this->container['config']["shaparak.providers.{$driver->value}"];
89 | }
90 |
91 | return $this->runtimeConfig;
92 | }
93 |
94 | /**
95 | * Create an instance of the specified driver.
96 | *
97 | * @return Provider
98 | */
99 | protected function createSamanDriver()
100 | {
101 | $config = $this->getConfig(ProviderName::SAMAN);
102 |
103 | return $this->buildProvider(
104 | SamanProvider::class,
105 | $config
106 | );
107 | }
108 |
109 | /**
110 | * Create an instance of the specified driver.
111 | *
112 | * @return Provider
113 | */
114 | protected function createParsianDriver()
115 | {
116 | $config = $this->getConfig(ProviderName::PARSIAN);
117 |
118 | return $this->buildProvider(
119 | ParsianProvider::class,
120 | $config
121 | );
122 | }
123 |
124 | /**
125 | * Create an instance of the specified driver.
126 | *
127 | * @return Provider
128 | */
129 | protected function createPasargadDriver()
130 | {
131 | $config = $this->getConfig(ProviderName::PASARGAD);
132 |
133 | return $this->buildProvider(
134 | PasargadProvider::class,
135 | $config
136 | );
137 | }
138 |
139 | /**
140 | * Create an instance of the specified driver.
141 | *
142 | * @return Provider
143 | */
144 | protected function createMellatDriver()
145 | {
146 | $config = $this->getConfig(ProviderName::MELLAT);
147 |
148 | return $this->buildProvider(
149 | MellatProvider::class,
150 | $config
151 | );
152 | }
153 |
154 | /**
155 | * Create an instance of the specified driver.
156 | *
157 | * @return Provider
158 | */
159 | protected function createMelliDriver()
160 | {
161 | $config = $this->getConfig(ProviderName::MELLI);
162 |
163 | return $this->buildProvider(
164 | MelliProvider::class,
165 | $config
166 | );
167 | }
168 |
169 | /**
170 | * Create an instance of the specified driver.
171 | *
172 | * @return Provider
173 | */
174 | protected function createSaderatDriver()
175 | {
176 | $config = $this->getConfig(ProviderName::SADERAT);
177 |
178 | return $this->buildProvider(
179 | SaderatProvider::class,
180 | $config
181 | );
182 | }
183 |
184 | /**
185 | * Create an instance of the specified driver.
186 | *
187 | * @return Provider
188 | */
189 | protected function createAsanPardakhtDriver()
190 | {
191 | $config = $this->getConfig(ProviderName::ASAN_PARDAKHT);
192 |
193 | return $this->buildProvider(
194 | AsanPardakhtProvider::class,
195 | $config
196 | );
197 | }
198 |
199 | /**
200 | * Create an instance of the specified driver.
201 | *
202 | * @return Provider
203 | */
204 | protected function createZarinpalDriver()
205 | {
206 | $config = $this->getConfig(ProviderName::ZARINPAL);
207 |
208 | return $this->buildProvider(
209 | ZarinpalProvider::class,
210 | $config
211 | );
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/Provider/ZarinpalProvider.php:
--------------------------------------------------------------------------------
1 | requestToken();
19 |
20 | return [
21 | 'gateway' => 'zarinpal',
22 | 'method' => 'GET',
23 | 'action' => $this->getUrlFor(self::URL_GATEWAY).'/'.$token,
24 | 'parameters' => [],
25 | ];
26 | }
27 |
28 | /**
29 | * {@inheritDoc}
30 | *
31 | * @throws Exception
32 | */
33 | protected function requestToken(): string
34 | {
35 | $transaction = $this->getTransaction();
36 |
37 | if ($this->getTransaction()->isReadyForTokenRequest() === false) {
38 | throw new Exception('transaction is not ready for requesting token from payment gateway');
39 | }
40 |
41 | $this->checkRequiredActionParameters([
42 | 'merchant_id',
43 | ]);
44 |
45 | $response = Http::acceptJson()->post($this->getUrlFor(self::URL_TOKEN), [
46 | 'merchant_id' => $this->getParameters('merchant_id'),
47 | 'callback_url' => $this->getCallbackUrl(),
48 | 'amount' => $this->getAmount(),
49 | 'description' => $this->getDescription(),
50 | ]);
51 |
52 | if ($response->successful()) {
53 | if ((int) $response->json('data.code') === 100) {
54 | $transaction->setGatewayToken($response->json('data.authority'), true); // update transaction
55 | return $response->json('data.authority');
56 | }
57 |
58 | $this->log($response->json('errors.message'), $response->json('errors'), 'error');
59 | throw new Exception(
60 | $response->json('errors.code').' '.$response->json('errors.message')
61 | );
62 | } else {
63 | $this->log($response->body());
64 | }
65 |
66 | throw new Exception('shaparak::shaparak.token_failed');
67 | }
68 |
69 | /**
70 | * {@inheritDoc}
71 | */
72 | public function getUrlFor(string $action = null): string
73 | {
74 | if ($this->environment === 'production') {
75 | switch ($action) {
76 | case self::URL_GATEWAY:
77 |
78 | return 'https://www.zarinpal.com/pg/StartPay';
79 |
80 | case self::URL_TOKEN:
81 |
82 | return 'https://api.zarinpal.com/pg/v4/payment/request.json';
83 |
84 | case self::URL_VERIFY:
85 |
86 | return 'https://api.zarinpal.com/pg/v4/payment/verify.json';
87 |
88 | }
89 | } else {
90 | switch ($action) {
91 | case self::URL_GATEWAY:
92 |
93 | return $this->bankTestBaseUrl.'/zarinpal/www.zarinpal.com/pg/StartPay';
94 |
95 | case self::URL_TOKEN:
96 |
97 | return $this->bankTestBaseUrl.'/zarinpal/api.zarinpal.com/pg/v4/payment/request.json';
98 |
99 | case self::URL_VERIFY:
100 |
101 | return $this->bankTestBaseUrl.'/zarinpal/api.zarinpal.com/pg/v4/payment/verify.json';
102 |
103 | }
104 | }
105 | throw new Exception('url destination is not valid!');
106 | }
107 |
108 | /**
109 | * {@inheritDoc}
110 | */
111 | public function canContinueWithCallbackParameters(): bool
112 | {
113 | try {
114 | $this->checkRequiredActionParameters([
115 | 'Authority',
116 | 'Status',
117 | ]);
118 | } catch (\Exception $e) {
119 | return false;
120 | }
121 |
122 | return $this->getParameters('Status') === 'OK';
123 | }
124 |
125 | /**
126 | * {@inheritDoc}
127 | *
128 | * @throws Exception
129 | */
130 | public function getGatewayReferenceId(): string
131 | {
132 | $this->checkRequiredActionParameters([
133 | 'Authority',
134 | ]);
135 |
136 | return $this->getParameters('Authority');
137 | }
138 |
139 | /**
140 | * {@inheritDoc}
141 | *
142 | * @throws Exception
143 | */
144 | public function verifyTransaction(): bool
145 | {
146 | if ($this->getTransaction()->isReadyForVerify() === false) {
147 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
148 | }
149 |
150 | $this->checkRequiredActionParameters([
151 | 'merchant_id',
152 | 'authority',
153 | ]);
154 |
155 | if ($this->getParameters('Status') !== 'OK') {
156 | throw new Exception('could not verify transaction with callback status: '.$this->getParameters('Status'));
157 | }
158 |
159 | $response = Http::acceptJson()->post($this->getUrlFor(self::URL_VERIFY), [
160 | 'merchant_id' => $this->getParameters('merchant_id'),
161 | 'authority' => $this->getParameters('authority'),
162 | 'amount' => $this->getTransaction()->getPayableAmount(),
163 | ]);
164 |
165 | if ($response->successful()) {
166 | if ((int) $response->json('data.code') === 100 || (int) $response->json('data.code') === 101) {
167 | $this->getTransaction()->setVerified(true); // save()
168 |
169 | return true;
170 | }
171 |
172 | $this->log($response->json('errors.message'), $response->json('errors'), 'error');
173 | throw new Exception(
174 | $response->json('errors.code').' '.$response->json('errors.message')
175 | );
176 | }
177 |
178 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
179 | }
180 |
181 | public function refundTransaction(): bool
182 | {
183 | return false;
184 | }
185 |
186 | private function getDescription(): string
187 | {
188 | $description = $this->getTransaction()->description;
189 | if (empty($description)) {
190 | $description = sprintf('Payment for Order ID: %s', $this->getTransaction()->id);
191 | }
192 |
193 | return $description;
194 | }
195 |
196 | protected function getGatewayOrderIdFromCallBackParameters(): string
197 | {
198 | return '';
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/Provider/MelliProvider.php:
--------------------------------------------------------------------------------
1 | getTransaction();
17 |
18 | if ($transaction->isReadyForTokenRequest() === false) {
19 | throw new Exception('transaction is not ready for requesting token from payment gateway');
20 | }
21 |
22 | $this->checkRequiredActionParameters([
23 | 'terminal_id',
24 | 'merchant_id',
25 | 'transaction_key',
26 | ]);
27 |
28 | $terminalId = $this->getParameters('terminal_id');
29 | $amount = $this->getAmount();
30 | $key = $this->getParameters('transaction_key');
31 | $orderId = $this->getGatewayOrderId();
32 |
33 | $response = Http::acceptJson()
34 | ->throw()
35 | ->post($this->getUrlFor(self::URL_TOKEN), [
36 | 'TerminalId' => $terminalId,
37 | 'MerchantId' => $this->getParameters('merchant_id'),
38 | 'Amount' => $amount,
39 | 'SignData' => $this->encryptPKCS7("{$terminalId};{$orderId};{$amount}", "{$key}"),
40 | 'ReturnUrl' => $this->getCallbackUrl(),
41 | 'LocalDateTime' => date('m/d/Y g:i:s a'),
42 | 'OrderId' => $orderId,
43 | ]);
44 |
45 | $resCode = $response->json('ResCode');
46 | if (is_numeric($resCode) && (int) $resCode === 0) {
47 | $transaction->setGatewayToken($response->json('Token'), true); // update transaction
48 |
49 | return $response->json('Token');
50 | }
51 |
52 | throw new Exception('shaparak::shaparak.token_failed');
53 | }
54 |
55 | /**
56 | * {@inheritDoc}
57 | *
58 | * @throws Exception
59 | */
60 | public function getFormParameters(): array
61 | {
62 | return [
63 | 'gateway' => 'melli',
64 | 'method' => 'get',
65 | 'action' => $this->getUrlFor(self::URL_GATEWAY),
66 | 'parameters' => [
67 | 'Token' => $this->requestToken(),
68 | ],
69 | ];
70 | }
71 |
72 | protected function getGatewayOrderIdFromCallBackParameters(): string
73 | {
74 | return (string) $this->getParameters('OrderId');
75 | }
76 |
77 | /**
78 | * {@inheritDoc}
79 | *
80 | * @throws Exception
81 | */
82 | public function verifyTransaction(): bool
83 | {
84 | if ($this->getTransaction()->isReadyForVerify() === false) {
85 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
86 | }
87 |
88 | $this->checkRequiredActionParameters([
89 | 'terminal_id',
90 | 'merchant_id',
91 | 'transaction_key',
92 | 'OrderId',
93 | 'Token',
94 | 'ResCode',
95 | ]);
96 |
97 | if ((int) $this->getParameters('ResCode') !== 0) {
98 | throw new Exception('could not verify transaction with ResCode: '.$this->getParameters('ResCode'));
99 | }
100 |
101 | $this->callbackAbuseCheckList();
102 |
103 | $key = $this->getParameters('transaction_key');
104 | $token = $this->getParameters('Token');
105 |
106 | $signature = $this->encryptPKCS7($token, $key);
107 |
108 | $response = Http::acceptJson()
109 | ->throw()
110 | ->post($this->getUrlFor(self::URL_VERIFY), [
111 | 'Token' => $token,
112 | 'SignData' => $signature,
113 | ]);
114 |
115 | $resCode = $response->json('ResCode');
116 | if (is_numeric($resCode) && (int) $resCode === 0 && (int) $response->json('Amount') === $this->getAmount()) {// got string token
117 | foreach ($response->json() as $k => $v) {
118 | $this->getTransaction()->addExtra($k, $v, false);
119 | }
120 | //$this->getTransaction()->setCardNumber($this->getParameters('accNoVal', @$response->accNoVal), false);
121 | $this->getTransaction()->setVerified(true);
122 |
123 | return true;
124 | }
125 |
126 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
127 | }
128 |
129 | /**
130 | * @throws Exception
131 | */
132 | public function refundTransaction(): bool
133 | {
134 | // TODO: Implement refundTransaction() method.
135 | throw new Exception('melli gateway does not support refund action right now');
136 | }
137 |
138 | /**
139 | * {@inheritDoc}
140 | */
141 | public function canContinueWithCallbackParameters(): bool
142 | {
143 | try {
144 | $this->checkRequiredActionParameters([
145 | 'OrderId',
146 | 'Token',
147 | 'ResCode',
148 | ]);
149 | } catch (\Exception $e) {
150 | return false;
151 | }
152 |
153 | return (int) $this->getParameters('ResCode') === 0;
154 | }
155 |
156 | /**
157 | * {@inheritDoc}
158 | */
159 | public function getGatewayReferenceId(): string
160 | {
161 | $this->checkRequiredActionParameters([
162 | 'Token',
163 | ]);
164 |
165 | return $this->getParameters('Token');
166 | }
167 |
168 | /**
169 | * {@inheritDoc}
170 | */
171 | public function getUrlFor(string $action = null): string
172 | {
173 | if ($this->environment === 'production') {
174 | switch ($action) {
175 | case self::URL_GATEWAY:
176 |
177 | return 'https://sadad.shaparak.ir/VPG/Purchase';
178 |
179 | case self::URL_TOKEN:
180 |
181 | return 'https://sadad.shaparak.ir/vpg/api/v0/Request/PaymentRequest';
182 |
183 | case self::URL_VERIFY:
184 |
185 | return 'https://sadad.shaparak.ir/vpg/api/v0/Advice/Verify';
186 |
187 | }
188 | } else {
189 | switch ($action) {
190 | case self::URL_GATEWAY:
191 |
192 | return $this->bankTestBaseUrl.'/melli/sadad.shaparak.ir/VPG/Purchase';
193 |
194 | case self::URL_TOKEN:
195 |
196 | return $this->bankTestBaseUrl.'/melli/sadad.shaparak.ir/VPG/api/v0/Request/PaymentRequest';
197 |
198 | case self::URL_VERIFY:
199 |
200 | return $this->bankTestBaseUrl.'/melli/sadad.shaparak.ir/VPG/api/v0/Advice/Verify';
201 |
202 | }
203 | }
204 |
205 | throw new Exception("could not find url for {$action} action");
206 | }
207 |
208 | /**
209 | * Create sign data based on (Tripledes(ECB,PKCS7)) algorithm
210 | *
211 | * @param string $str data
212 | * @param string $key key
213 | */
214 | protected function encryptPKCS7(string $str, string $key): string
215 | {
216 | $key = base64_decode($key);
217 | $cipherText = openssl_encrypt($str, 'DES-EDE3', $key, OPENSSL_RAW_DATA);
218 |
219 | return base64_encode($cipherText);
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/Provider/SaderatProvider.php:
--------------------------------------------------------------------------------
1 | 'saderat',
19 | 'method' => 'post',
20 | 'action' => $this->getUrlFor(self::URL_GATEWAY),
21 | 'parameters' => [
22 | 'token' => $this->requestToken(),
23 | 'TerminalID' => $this->getParameters('terminal_id'),
24 | ],
25 | ];
26 | }
27 |
28 | /**
29 | * {@inheritDoc}
30 | */
31 | public function getUrlFor(string $action = null): string
32 | {
33 | if ($this->environment === 'production') {
34 | switch ($action) {
35 | case self::URL_GATEWAY:
36 |
37 | return 'https://sepehr.shaparak.ir:8080/Pay';
38 |
39 | case self::URL_TOKEN:
40 |
41 | return 'https://sepehr.shaparak.ir:8081/V1/PeymentApi/GetToken';
42 |
43 | case self::URL_VERIFY:
44 |
45 | return 'https://sepehr.shaparak.ir:8081/V1/PeymentApi/Advice';
46 |
47 | case self::URL_REFUND:
48 |
49 | return 'https://sepehr.shaparak.ir:8081/V1/PeymentApi/Rollback';
50 |
51 | }
52 | } else {
53 | switch ($action) {
54 | case self::URL_GATEWAY:
55 |
56 | return $this->bankTestBaseUrl.'/saderat/sepehr.shaparak.ir/Pay';
57 |
58 | case self::URL_TOKEN:
59 |
60 | return $this->bankTestBaseUrl.'/saderat/sepehr.shaparak.ir/V1/PeymentApi/GetToken';
61 |
62 | case self::URL_VERIFY:
63 |
64 | return $this->bankTestBaseUrl.'/saderat/sepehr.shaparak.ir/V1/PeymentApi/Advice';
65 |
66 | case self::URL_REFUND:
67 |
68 | return $this->bankTestBaseUrl.'/saderat/sepehr.shaparak.ir/V1/PeymentApi/Rollback';
69 |
70 | }
71 | }
72 | throw new Exception("could not find url for {$action} action");
73 | }
74 |
75 | /**
76 | * {@inheritDoc}
77 | *
78 | * @throws Exception
79 | * @throws JsonException
80 | */
81 | protected function requestToken(): string
82 | {
83 | $this->log(__METHOD__);
84 |
85 | if ($this->getTransaction()->isReadyForTokenRequest() === false) {
86 | throw new Exception('transaction is not ready for requesting token from payment gateway');
87 | }
88 |
89 | $this->checkRequiredActionParameters([
90 | 'terminal_id',
91 | ]);
92 |
93 | $response = Http::acceptJson()
94 | ->throw()
95 | ->post($this->getUrlFor(self::URL_TOKEN), [
96 | 'Amount' => $this->getAmount(),
97 | 'callbackUrl' => $this->getCallbackUrl(),
98 | 'invoiceID' => $this->getGatewayOrderId(),
99 | 'terminalID' => $this->getParameters('terminal_id'),
100 | ]);
101 |
102 | if ($response->json('Status') === 0) {
103 | // got string token
104 | $this->getTransaction()->setGatewayToken(
105 | $response['Accesstoken'],
106 | true
107 | ); // update transaction
108 |
109 | return $response->json('Accesstoken');
110 | }
111 |
112 | throw new Exception('shaparak::shaparak.token_failed');
113 | }
114 |
115 | protected function getGatewayOrderIdFromCallBackParameters(): string
116 | {
117 | return (string) $this->getParameters('InvoiceId');
118 | }
119 |
120 | /**
121 | * {@inheritDoc}
122 | */
123 | public function verifyTransaction(): bool
124 | {
125 | if ($this->getTransaction()->isReadyForVerify() === false) {
126 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
127 | }
128 |
129 | $this->checkRequiredActionParameters([
130 | 'terminal_id',
131 | 'RespCode',
132 | 'RespMsg',
133 | 'Amount',
134 | 'InvoiceId',
135 | 'TerminalId',
136 | 'TraceNumber',
137 | 'DatePaid',
138 | 'DigitalReceipt',
139 | 'IssuerBank',
140 | 'CardNumber',
141 | ]);
142 |
143 | $this->callbackAbuseCheckList();
144 |
145 | $response = Http::acceptJson()
146 | ->throw()
147 | ->post($this->getUrlFor(self::URL_VERIFY), [
148 | 'digitalreceipt' => $this->getParameters('DigitalReceipt'),
149 | 'Tid' => $this->getParameters('terminal_id'),
150 | ]);
151 |
152 | if (in_array($response->json('Status'), ['OK', 'Duplicate'])) {
153 | if ((int) $response->json('ReturnId') === $this->transaction->getPayableAmount()) {
154 | return $this->getTransaction()->setVerified();
155 | }
156 |
157 | throw new Exception('shaparak::shaparak.amounts_not_match');
158 | }
159 |
160 | throw new Exception('shaparak::shaparak.verify_failed');
161 | }
162 |
163 | /**
164 | * {@inheritDoc}
165 | */
166 | public function refundTransaction(): bool
167 | {
168 | if ($this->getTransaction()->isReadyForRefund() == false) {
169 | throw new Exception('shaparak::shaparak.could_not_refund_transaction');
170 | }
171 |
172 | $this->checkRequiredActionParameters([
173 | 'terminal_id',
174 | 'RespCode',
175 | 'RespMsg',
176 | 'Amount',
177 | 'InvoiceId',
178 | 'TerminalId',
179 | 'TraceNumber',
180 | 'DatePaid',
181 | 'DigitalReceipt',
182 | 'IssuerBank',
183 | 'CardNumber',
184 | ]);
185 |
186 | $response = Http::acceptJson()
187 | ->throw()
188 | ->post($this->getUrlFor(self::URL_REFUND), [
189 | 'digitalreceipt' => $this->getParameters('DigitalReceipt'),
190 | 'Tid' => $this->getParameters('terminal_id'),
191 | ]);
192 |
193 | if (in_array($response->json('Status'), ['OK', 'Duplicate'])) {
194 | if ((int) $response->json('ReturnId') === $this->transaction->getPayableAmount()) {
195 | return $this->getTransaction()->setRefunded();
196 | }
197 |
198 | throw new Exception('shaparak::shaparak.amounts_not_match');
199 | }
200 |
201 | throw new Exception('shaparak::shaparak.refund_failed');
202 | }
203 |
204 | /**
205 | * {@inheritDoc}
206 | */
207 | public function canContinueWithCallbackParameters(): bool
208 | {
209 | try {
210 | $this->checkRequiredActionParameters([
211 | 'RespCode',
212 | ]);
213 | } catch (\Exception $e) {
214 | return false;
215 | }
216 |
217 | $respCode = $this->getParameters('RespCode');
218 |
219 | return is_numeric($respCode) && (int) $respCode === 0;
220 | }
221 |
222 | /**
223 | * {@inheritDoc}
224 | */
225 | public function getGatewayReferenceId(): string
226 | {
227 | $this->checkRequiredActionParameters([
228 | 'RRN',
229 | ]);
230 |
231 | return $this->getParameters('RRN');
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/src/Provider/SamanProvider.php:
--------------------------------------------------------------------------------
1 | getTransaction();
19 |
20 | if ($transaction->isReadyForTokenRequest() === false) {
21 | throw new Exception('transaction is not ready for requesting token from payment gateway');
22 | }
23 |
24 | $this->checkRequiredActionParameters([
25 | 'terminal_id',
26 | ]);
27 |
28 | $sendParams = [
29 | 'TermID' => $this->getParameters('terminal_id'),
30 | 'ResNum' => $this->getGatewayOrderId(), // get it from Transaction
31 | 'TotalAmount' => $this->getAmount(),
32 | ];
33 |
34 | try {
35 | $soapClient = $this->getSoapClient(self::URL_TOKEN);
36 |
37 | $response = $soapClient->__soapCall('RequestToken', $sendParams);
38 |
39 | if (! empty($response)) {
40 | $token = trim($response);
41 | if (strlen($token) >= 20) { // got string token
42 | $this->log("fetched token from gateway: {$token}");
43 | $transaction->setGatewayToken($token, true); // update transaction
44 |
45 | return $token;
46 | }
47 |
48 | throw new Exception(sprintf('shaparak::saman.error_%s', $response));
49 | }
50 |
51 | throw new Exception('shaparak::shaparak.token_failed');
52 | } catch (SoapFault $e) {
53 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
54 | }
55 | }
56 |
57 | /**
58 | * {@inheritDoc}
59 | *
60 | * @throws Exception
61 | */
62 | public function getFormParameters(): array
63 | {
64 | $token = $this->requestToken();
65 |
66 | return [
67 | 'gateway' => 'saman',
68 | 'method' => 'POST',
69 | 'action' => $this->getUrlFor(self::URL_GATEWAY),
70 | 'parameters' => [
71 | 'Token' => $token,
72 | 'RedirectURL' => $this->getCallbackUrl(),
73 | 'getmethod' => (bool) $this->getParameters('get_method', true),
74 | ],
75 | ];
76 | }
77 |
78 | protected function getGatewayOrderIdFromCallBackParameters(): string
79 | {
80 | return (string) $this->getParameters('ResNum');
81 | }
82 |
83 | /**
84 | * {@inheritDoc}
85 | *
86 | * @throws Exception
87 | */
88 | public function verifyTransaction(): bool
89 | {
90 | if ($this->getTransaction()->isReadyForVerify() === false) {
91 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
92 | }
93 |
94 | $this->checkRequiredActionParameters([
95 | 'terminal_id',
96 | 'State',
97 | 'StateCode',
98 | 'RefNum',
99 | 'ResNum',
100 | 'TraceNo',
101 | 'SecurePan',
102 | 'CID',
103 | ]);
104 |
105 | if ($this->getParameters('State') !== 'OK') {
106 | throw new Exception('could not verify transaction with callback state: '.$this->getParameters('State'));
107 | }
108 |
109 | $this->callbackAbuseCheckList();
110 |
111 | try {
112 | $soapClient = $this->getSoapClient(self::URL_VERIFY);
113 |
114 | $response = $soapClient->VerifyTransaction(
115 | $this->getParameters('RefNum'),
116 | $this->getParameters('terminal_id')
117 | );
118 |
119 | if (isset($response)) {
120 | if ($response > 0 && ($response - $this->getTransaction()->getPayableAmount() < PHP_FLOAT_EPSILON)) {
121 | // double-check the amount by transaction amount
122 | $this->getTransaction()->setCardNumber($this->getParameters('SecurePan'), false); // no save()
123 | $this->getTransaction()->setVerified(true); // save()
124 |
125 | return true;
126 | }
127 |
128 | throw new Exception('shaparak::saman.error_'.(string) $response);
129 | }
130 |
131 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
132 | } catch (SoapFault $e) {
133 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
134 | }
135 | }
136 |
137 | /**
138 | * {@inheritDoc}
139 | *
140 | * @throws Exception
141 | */
142 | public function refundTransaction(): bool
143 | {
144 | if ($this->refundSupport === false || $this->getTransaction()->isReadyForRefund() === false) {
145 | throw new Exception('shaparak::shaparak.could_not_refund_payment');
146 | }
147 |
148 | $this->checkRequiredActionParameters([
149 | 'RefNum',
150 | 'terminal_id',
151 | 'terminal_pass',
152 | ]);
153 |
154 | try {
155 | $soapClient = $this->getSoapClient(self::URL_REFUND);
156 |
157 | $refundAmount = $this->getAmount(); // total amount
158 |
159 | $response = $soapClient->reverseTransaction1(
160 | $this->getParameters('RefNum'),
161 | $this->getParameters('terminal_id'),
162 | $this->getParameters('terminal_pass'),
163 | $refundAmount
164 | );
165 |
166 | if (isset($response)) {
167 | if ($response == 1) { // check by transaction amount
168 | $this->getTransaction()->setRefunded(true);
169 |
170 | return true;
171 | }
172 |
173 | throw new Exception('shaparak::saman.error_'.strval($response));
174 | }
175 |
176 | throw new Exception('shaparak::shaparak.could_not_refund_payment');
177 | } catch (SoapFault $e) {
178 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
179 | }
180 | }
181 |
182 | /**
183 | * {@inheritDoc}
184 | */
185 | public function canContinueWithCallbackParameters(): bool
186 | {
187 | try {
188 | $this->checkRequiredActionParameters([
189 | 'RefNum',
190 | 'State',
191 | ]);
192 | } catch (\Exception $e) {
193 | return false;
194 | }
195 |
196 | return $this->getParameters('State') === 'OK';
197 | }
198 |
199 | /**
200 | * {@inheritDoc}
201 | *
202 | * @throws Exception
203 | */
204 | public function getGatewayReferenceId(): string
205 | {
206 | $this->checkRequiredActionParameters([
207 | 'RefNum',
208 | ]);
209 |
210 | return $this->getParameters('RefNum');
211 | }
212 |
213 | /**
214 | * {@inheritDoc}
215 | */
216 | public function getUrlFor(string $action = null): string
217 | {
218 | if ($this->environment === 'production') {
219 | return match ($action) {
220 | self::URL_GATEWAY => 'https://sep.shaparak.ir/Payment.aspx',
221 | self::URL_TOKEN => 'https://sep.shaparak.ir/Payments/InitPayment.asmx?WSDL',
222 | default => 'https://sep.shaparak.ir/payments/referencepayment.asmx?WSDL',
223 | };
224 | }
225 |
226 | return match ($action) {
227 | self::URL_GATEWAY => $this->bankTestBaseUrl . '/saman/sep.shaparak.ir/payment.aspx',
228 | self::URL_TOKEN => $this->bankTestBaseUrl . '/saman/sep.shaparak.ir/payments/initpayment.asmx?wsdl',
229 | default => $this->bankTestBaseUrl . '/saman/sep.shaparak.ir/payments/referencepayment.asmx?wsdl',
230 | };
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/src/Provider/AbstractProvider.php:
--------------------------------------------------------------------------------
1 | environment = $environment;
82 | $this->transaction = $transaction;
83 | $this->httpClientOptions = $httpClientOptions;
84 | $this->bankTestBaseUrl = config('shaparak.banktest_base_url');
85 | $this->setParameters($configs);
86 | }
87 |
88 | /**
89 | * {@inheritDoc}
90 | */
91 | public function getForm(): \Illuminate\View\View
92 | {
93 | $formParameters = $this->getFormParameters();
94 |
95 | return view(
96 | 'shaparak::goto-gate-form',
97 | array_merge($formParameters, [
98 | 'buttonLabel' => $this->getParameters('submit_label') ?: __('shaparak::shaparak.goto_gate'),
99 | 'autoSubmit' => (bool) $this->getParameters('auto_submit', true),
100 | ])
101 | );
102 | }
103 |
104 | /**
105 | * {@inheritDoc}
106 | */
107 | abstract public function getFormParameters(): array;
108 |
109 | /**
110 | * {@inheritDoc}
111 | */
112 | public function getParameters(string $key = null, $default = null)
113 | {
114 | if (is_null($key)) {
115 | return $this->parameters;
116 | }
117 |
118 | $key = strtolower($key);
119 |
120 | return $this->parameters[$key] ?? $default;
121 | }
122 |
123 | /**
124 | * {@inheritDoc}
125 | */
126 | public function setParameters(array $parameters = []): ProviderContract
127 | {
128 | $parameters = array_change_key_case($parameters);
129 |
130 | $this->parameters = array_merge($this->parameters, $parameters);
131 |
132 | return $this;
133 | }
134 |
135 | /**
136 | * {@inheritDoc}
137 | */
138 | abstract public function verifyTransaction(): bool;
139 |
140 | /**
141 | * {@inheritDoc}
142 | */
143 | public function settleTransaction(): bool
144 | {
145 | // default behavior
146 | return $this->getTransaction()->setSettled();
147 | }
148 |
149 | /**
150 | * {@inheritDoc}
151 | */
152 | public function accomplishTransaction(): bool
153 | {
154 | // default behavior
155 | return $this->getTransaction()->setAccomplished();
156 | }
157 |
158 | public function getTransaction(): Transaction
159 | {
160 | return $this->transaction;
161 | }
162 |
163 | /**
164 | * {@inheritDoc}
165 | */
166 | abstract public function refundTransaction(): bool;
167 |
168 | /**
169 | * {@inheritDoc}
170 | */
171 | abstract public function getGatewayReferenceId(): string;
172 |
173 | /**
174 | * {@inheritDoc}
175 | */
176 | abstract public function canContinueWithCallbackParameters(): bool;
177 |
178 | /**
179 | * {@inheritDoc}
180 | */
181 | public function refundSupport(): bool
182 | {
183 | return $this->refundSupport;
184 | }
185 |
186 | /**
187 | * {@inheritDoc}
188 | */
189 | public function settlementSupport(): bool
190 | {
191 | return $this->settlementSupport;
192 | }
193 |
194 | /**
195 | * {@inheritDoc}
196 | */
197 | public function checkRequiredActionParameters(array $parameters): void
198 | {
199 | $parameters = array_map('strtolower', $parameters);
200 |
201 | foreach ($parameters as $parameter) {
202 | if (! array_key_exists($parameter, $this->parameters) || trim($this->parameters[$parameter]) === '') {
203 | throw new Exception("Parameters array must have a not null value for key: '$parameter'");
204 | }
205 | }
206 | }
207 |
208 | /**
209 | * @throws SoapFault|Exception
210 | */
211 | protected function getSoapClient(string $action): SoapClient
212 | {
213 | $soapOptions = $this->httpClientOptions ? $this->httpClientOptions['soap'] : [];
214 |
215 | // set soap options if require. see shaparak config
216 | return new SoapClient($this->getUrlFor($action), $soapOptions);
217 | }
218 |
219 | /**
220 | * {@inheritDoc}
221 | */
222 | abstract public function getUrlFor(string $action): string;
223 |
224 | /**
225 | * fetches callback url from parameters
226 | */
227 | protected function getCallbackUrl(): string
228 | {
229 | return Str::is('http*', $this->getParameters('callback_url')) ?
230 | $this->getParameters('callback_url') :
231 | $this->getTransaction()->getCallbackUrl();
232 | }
233 |
234 | /**
235 | * fetches payable amount of the transaction
236 | */
237 | protected function getAmount(): int
238 | {
239 | return (is_int($this->getParameters('amount')) && ! empty($this->getParameters('amount'))) ?
240 | $this->getParameters('amount') :
241 | $this->getTransaction()->getPayableAmount();
242 | }
243 |
244 | /**
245 | * fetches payable amount of the transaction
246 | */
247 | protected function getGatewayOrderId(): int
248 | {
249 | return $this->getTransaction()->getGatewayOrderId();
250 | }
251 |
252 | abstract protected function getGatewayOrderIdFromCallBackParameters(): string;
253 |
254 | protected function callbackAbuseCheckList(): void
255 | {
256 | if ((string) $this->getTransaction()->gateway_order_id !== $this->getGatewayOrderIdFromCallBackParameters()) {
257 | throw new Exception('shaparak::shaparak.could_not_pass_abuse_checklist');
258 | }
259 | }
260 |
261 | protected function log(string $message, array $params = [], string $level = 'debug'): void
262 | {
263 | $reflect = new ReflectionClass($this);
264 | $provider = strtolower(str_replace('Provider', '', $reflect->getShortName()));
265 |
266 | $message = $provider.': '.$message;
267 |
268 | Shaparak::log($message, $params, $level);
269 | }
270 |
271 | public function getTransactionResult(): bool
272 | {
273 | return $this->provideTransactionResult;
274 | }
275 |
276 | public function setCallBackParameters(array $parameters): bool
277 | {
278 | return $this->getTransaction()->setCallBackParameters($parameters);
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/src/Provider/ParsianProvider.php:
--------------------------------------------------------------------------------
1 | getTransaction();
23 |
24 | if ($transaction->isReadyForTokenRequest() === false) {
25 | throw new Exception('transaction is not ready for requesting token from payment gateway');
26 | }
27 |
28 | $this->checkRequiredActionParameters([
29 | 'pin',
30 | ]);
31 |
32 | $sendParams = [
33 | 'LoginAccount' => $this->getParameters('pin'),
34 | 'Amount' => $transaction->getPayableAmount(),
35 | 'OrderId' => $transaction->getGatewayOrderId(),
36 | 'CallBackUrl' => $this->getCallbackUrl(),
37 | 'AdditionalData' => (string) $this->getParameters('additional_date'),
38 | ];
39 |
40 | try {
41 | $soapClient = $this->getSoapClient(self::URL_SALE);
42 |
43 | $response = $soapClient->SalePaymentRequest(['requestData' => $sendParams]);
44 |
45 | if (isset($response->SalePaymentRequestResult,
46 | $response->SalePaymentRequestResult->Status,
47 | $response->SalePaymentRequestResult->Token)) {
48 | if ((int) $response->SalePaymentRequestResult->Status === 0) {
49 | $this->log("fetched token from gateway: {$response->SalePaymentRequestResult->Token}");
50 | $this->getTransaction()->setGatewayToken($response->SalePaymentRequestResult->Token);
51 |
52 | return $response->SalePaymentRequestResult->Token;
53 | }
54 |
55 | throw new Exception(sprintf('shaparak::parsian.error_%s',
56 | $response->SalePaymentRequestResult->Status));
57 | }
58 |
59 | throw new Exception('shaparak::shaparak.token_failed');
60 | } catch (SoapFault $e) {
61 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
62 | }
63 | }
64 |
65 | /**
66 | * {@inheritDoc}
67 | *
68 | * @throws Exception
69 | */
70 | public function getFormParameters(): array
71 | {
72 | $token = $this->requestToken();
73 |
74 | return [
75 | 'gateway' => 'parsian',
76 | 'method' => 'get',
77 | 'action' => $this->getUrlFor(self::URL_GATEWAY),
78 | 'parameters' => [
79 | 'token' => $token,
80 | ],
81 | ];
82 | }
83 |
84 | protected function getGatewayOrderIdFromCallBackParameters(): string
85 | {
86 | return (string) $this->getParameters('OrderId');
87 | }
88 |
89 | /**
90 | * {@inheritDoc}
91 | *
92 | * @throws Exception
93 | */
94 | public function verifyTransaction(): bool
95 | {
96 | if ($this->getTransaction()->isReadyForVerify() === false) {
97 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
98 | }
99 |
100 | $this->checkRequiredActionParameters([
101 | 'pin',
102 | 'Token',
103 | 'Status',
104 | ]);
105 |
106 | $this->callbackAbuseCheckList();
107 |
108 | if ((int) $this->getParameters('Status') !== 0) {
109 | throw new Exception(
110 | 'could not verify transaction with callback state: '.$this->getParameters('Status')
111 | );
112 | }
113 |
114 | try {
115 | $sendParams = [
116 | 'LoginAccount' => $this->getParameters('pin'),
117 | 'Token' => $this->getParameters('Token'),
118 | ];
119 |
120 | $soapClient = $this->getSoapClient(self::URL_CONFIRM);
121 |
122 | $response = $soapClient->ConfirmPayment(['requestData' => $sendParams]);
123 |
124 | if (isset($response->ConfirmPaymentResult, $response->ConfirmPaymentResult->Status)) {
125 | if ((int) $response->ConfirmPaymentResult->Status === 0) {
126 | //$this->getTransaction()->setCardNumber($this->getParameters('CardNumberMasked'), false); // no save()
127 | $this->getTransaction()->setVerified(true); // save()
128 |
129 | return true;
130 | }
131 |
132 | throw new Exception(
133 | sprintf('shaparak::parsian.error_%s', $response->ConfirmPaymentResult->Status)
134 | );
135 | }
136 |
137 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
138 | } catch (SoapFault $e) {
139 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
140 | }
141 | }
142 |
143 | /**
144 | * {@inheritDoc}
145 | *
146 | * @throws Exception
147 | */
148 | public function refundTransaction(): bool
149 | {
150 | if ($this->getTransaction()->isReadyForRefund() === false) {
151 | throw new Exception('shaparak::shaparak.could_not_refund_payment');
152 | }
153 |
154 | $this->checkRequiredActionParameters([
155 | 'pin',
156 | 'Token',
157 | ]);
158 |
159 | try {
160 | $sendParams = [
161 | 'LoginAccount' => $this->getParameters('pin'),
162 | 'Token' => $this->getParameters('Token'),
163 | ];
164 |
165 | $soapClient = $this->getSoapClient(self::URL_REFUND);
166 |
167 | $response = $soapClient->ReversalRequest(['requestData' => $sendParams]);
168 |
169 | if (isset($response->ReversalRequestResult, $response->ReversalRequestResult->Status)) {
170 | if ((int) $response->ReversalRequestResult->Status === 0) {
171 | $this->getTransaction()->setRefunded(true);
172 |
173 | return true;
174 | }
175 |
176 | throw new Exception(
177 | sprintf('shaparak::parsian.error_%s', $response->ReversalRequestResult->Status)
178 | );
179 | }
180 |
181 | throw new Exception('larapay::parsian.errors.invalid_response');
182 | } catch (SoapFault $e) {
183 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
184 | }
185 | }
186 |
187 | /**
188 | * {@inheritDoc}
189 | */
190 | public function canContinueWithCallbackParameters(): bool
191 | {
192 | try {
193 | $this->checkRequiredActionParameters([
194 | 'Token',
195 | 'Status',
196 | ]);
197 | } catch (\Exception $e) {
198 | return false;
199 | }
200 |
201 | return (int) $this->getParameters('Status') === 0;
202 | }
203 |
204 | /**
205 | * {@inheritDoc}
206 | *
207 | * @throws Exception
208 | */
209 | public function getGatewayReferenceId(): string
210 | {
211 | $this->checkRequiredActionParameters([
212 | 'RRN',
213 | ]);
214 |
215 | return $this->getParameters('RRN');
216 | }
217 |
218 | /**
219 | * {@inheritDoc}
220 | */
221 | public function getUrlFor(string $action): string
222 | {
223 | if ($this->environment === 'production') {
224 | switch ($action) {
225 | case self::URL_GATEWAY:
226 |
227 | return 'https://pec.shaparak.ir/NewIPG/';
228 |
229 | case self::URL_SALE:
230 |
231 | return 'https://pec.shaparak.ir/NewIPGServices/Sale/SaleService.asmx?WSDL';
232 |
233 | case self::URL_CONFIRM:
234 |
235 | return 'https://pec.shaparak.ir/NewIPGServices/Confirm/ConfirmService.asmx?WSDL';
236 |
237 | case self::URL_REFUND:
238 |
239 | return 'https://pec.shaparak.ir/NewIPGServices/Reverse/ReversalService.asmx?WSDL';
240 |
241 | case self::URL_MULTIPLEX:
242 |
243 | return 'https://pec.shaparak.ir/NewIPGServices/MultiplexedSale/OnlineMultiplexedSalePaymentService.asmx?WSDL';
244 |
245 | }
246 | } else {
247 | switch ($action) {
248 | case self::URL_GATEWAY:
249 |
250 | return $this->bankTestBaseUrl.'/parsian/pec.shaparak.ir/NewIPG';
251 |
252 | case self::URL_SALE:
253 |
254 | return $this->bankTestBaseUrl.'/parsian/pec.shaparak.ir/NewIPGServices/Sale/SaleService.asmx?wsdl';
255 |
256 | case self::URL_CONFIRM:
257 |
258 | return $this->bankTestBaseUrl.'/parsian/pec.shaparak.ir/NewIPGServices/Confirm/ConfirmService.asmx?wsdl';
259 |
260 | case self::URL_REFUND:
261 |
262 | return $this->bankTestBaseUrl.'/parsian/pec.shaparak.ir/NewIPGServices/Reverse/ReversalService.asmx?wsdl';
263 |
264 | case self::URL_MULTIPLEX:
265 |
266 | return $this->bankTestBaseUrl.'/parsian/pec.shaparak.ir/NewIPGServices/MultiplexedSale/OnlineMultiplexedSalePaymentService.asmx?wsdl';
267 |
268 | }
269 | }
270 | throw new Exception("could not find url for {$action} action");
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/src/Provider/PasargadProvider.php:
--------------------------------------------------------------------------------
1 | checkRequiredActionParameters([
29 | 'terminal_id',
30 | 'merchant_id',
31 | 'certificate_path',
32 | ]);
33 |
34 | return [
35 | 'gateway' => 'pasargad',
36 | 'method' => 'GET',
37 | 'action' => $this->getUrlFor(self::URL_GATEWAY),
38 | 'parameters' => [
39 | 'n' => $this->requestToken(),
40 | ],
41 | ];
42 | }
43 |
44 | /**
45 | * {@inheritDoc}
46 | *
47 | * @throws Exception
48 | */
49 | public function requestToken(): string
50 | {
51 | $this->log(__METHOD__);
52 | $transaction = $this->getTransaction();
53 |
54 | if ($transaction->isReadyForTokenRequest() === false) {
55 | throw new Exception('transaction is not ready for requesting token from payment gateway');
56 | }
57 |
58 | $this->checkRequiredActionParameters([
59 | 'terminal_id',
60 | 'merchant_id',
61 | 'certificate_path',
62 | ]);
63 |
64 | $response = $this->callApi($this->getUrlFor(self::URL_TOKEN), [
65 | 'invoicenumber' => $this->getGatewayOrderId(),
66 | 'invoiceDate' => date('Y/m/d H:i:s', strtotime($this->getTransaction()->created_at)),
67 | 'amount' => $this->getAmount(),
68 | 'terminalCode' => $this->getParameters('terminal_id'),
69 | 'merchantCode' => $this->getParameters('merchant_id'),
70 | 'redirectAddress' => $this->getCallbackUrl(),
71 | 'timeStamp' => date('Y/m/d H:i:s'),
72 | 'action' => self::ACTION_GET_TOKEN,
73 | 'subpaymentlist' => $this->getParameters('subpaymentlist'),
74 | ]);
75 |
76 | $this->log(__METHOD__, $response);
77 |
78 | return $response['Token'];
79 | }
80 |
81 | protected function getGatewayOrderIdFromCallBackParameters(): string
82 | {
83 | return (string) $this->getParameters('iN');
84 | }
85 |
86 | /**
87 | * {@inheritDoc}
88 | * @throws Exception
89 | */
90 | public function verifyTransaction(): bool
91 | {
92 | $this->log(__METHOD__);
93 |
94 | if ($this->getTransaction()->isReadyForVerify() === false) {
95 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
96 | }
97 |
98 | $this->checkRequiredActionParameters([
99 | 'terminal_id',
100 | 'merchant_id',
101 | 'certificate_path',
102 | 'iN',
103 | 'iD',
104 | 'tref',
105 | ]);
106 |
107 | $this->callbackAbuseCheckList();
108 |
109 | // update transaction reference number
110 | if (! empty($this->getParameters('tref'))) {
111 | $this->getTransaction()->setGatewayToken(
112 | $this->getParameters('tref'),
113 | true
114 | ); // update transaction
115 | } else {
116 | throw new Exception('could not verify transaction without tref');
117 | }
118 |
119 | $response = $this->callApi($this->getUrlFor(self::URL_VERIFY), [
120 | 'merchantCode' => $this->getParameters('merchant_id'),
121 | 'terminalCode' => $this->getParameters('terminal_id'),
122 | 'invoiceNumber' => $this->getParameters('iN'),
123 | 'invoiceDate' => $this->getParameters('iD'),
124 | 'amount' => $this->getAmount(),
125 | 'timeStamp' => date('Y/m/d H:i:s'),
126 | ]);
127 |
128 | $this->log(__METHOD__, $response);
129 |
130 | if ($response['IsSuccess'] === true) {
131 | $this->getTransaction()->setCardNumber($response['MaskedCardNumber'], false); // no save()
132 | $this->getTransaction()->addExtra('HashedCardNumber', $response['HashedCardNumber'], false);
133 | $this->getTransaction()->addExtra('ShaparakRefNumber', $response['ShaparakRefNumber'], false);
134 | $this->getTransaction()->setVerified();
135 |
136 | return true;
137 | }
138 |
139 | throw new Exception('shaparak::shaparak.verify_failed');
140 | }
141 |
142 | /**
143 | * {@inheritDoc}
144 | * @throws Exception
145 | */
146 | public function refundTransaction(): bool
147 | {
148 | $this->log(__METHOD__);
149 |
150 | if ($this->getTransaction()->isReadyForRefund() === false) {
151 | throw new Exception('shaparak::shaparak.could_not_refund_transaction');
152 | }
153 |
154 | $this->checkRequiredActionParameters([
155 | 'terminal_id',
156 | 'merchant_id',
157 | 'certificate_path',
158 | 'iN',
159 | 'iD',
160 | 'tref',
161 | ]);
162 |
163 | $response = $this->callApi($this->getUrlFor(self::URL_REFUND), [
164 | 'invoiceNumber' => $this->getParameters('iN'),
165 | 'invoiceDate' => $this->getParameters('iD'),
166 | 'terminalCode' => $this->getParameters('terminal_id'),
167 | 'merchantCode' => $this->getParameters('merchant_id'),
168 | 'timeStamp' => date('Y/m/d H:i:s'),
169 | ]);
170 |
171 | $this->log(__METHOD__, $response);
172 |
173 | if ($response['IsSuccess'] === true) {
174 | $this->getTransaction()->setRefunded();
175 |
176 | return true;
177 | }
178 |
179 | throw new Exception('shaparak::shaparak.refund_failed');
180 | }
181 |
182 | private function callApi(string $url, array $body, $method = 'post'): array
183 | {
184 | $sign = $this->sign(json_encode($body));
185 |
186 | $this->log("callApi({$url}) Sign: {$sign}", $body);
187 |
188 | $response = Http::contentType('application/json')
189 | ->acceptJson()
190 | ->withHeaders([
191 | 'Sign' => $sign,
192 | ])
193 | ->throw()
194 | ->{$method}($url, $body);
195 |
196 | $this->log($response->json('Message'), $response->json());
197 |
198 | if ($response->json('IsSuccess') === false) {
199 | throw new Exception(($response->json('Message') ?? 'Invalid Pasargad response!'));
200 | }
201 |
202 | return $response->json();
203 | }
204 |
205 | private function sign(string $string): string
206 | {
207 | $string = sha1($string, true);
208 | $string = $this->getProcessor()->sign($string); // digital signature
209 |
210 | return base64_encode($string); // base64_encode
211 | }
212 |
213 | /**
214 | * @param string|null $certificatePath
215 | * @return RSAProcessor
216 | */
217 | private function getProcessor(string $certificatePath = null): RSAProcessor
218 | {
219 | $path = $certificatePath ?: $this->getParameters('certificate_path');
220 |
221 | return new RSAProcessor($path, RSAKeyType::XMLFile);
222 | }
223 |
224 | /**
225 | * {@inheritDoc}
226 | */
227 | public function canContinueWithCallbackParameters(): bool
228 | {
229 | try {
230 | $this->checkRequiredActionParameters([
231 | 'iN',
232 | 'iD',
233 | 'tref',
234 | ]);
235 | } catch (\Exception $e) {
236 | return false;
237 | }
238 |
239 | if (! empty($this->getParameters('tref'))) {
240 | return true;
241 | }
242 |
243 | return false;
244 | }
245 |
246 | /**
247 | * {@inheritDoc}
248 | */
249 | public function getGatewayReferenceId(): string
250 | {
251 | $this->checkRequiredActionParameters([
252 | 'tref',
253 | ]);
254 |
255 | return $this->getParameters('tref');
256 | }
257 |
258 | /**
259 | * {@inheritDoc}
260 | */
261 | public function getUrlFor(string $action): string
262 | {
263 | if ($this->environment === 'production') {
264 | switch ($action) {
265 | case self::URL_GATEWAY:
266 |
267 | return 'https://pep.shaparak.ir/gateway.aspx';
268 |
269 | case self::URL_TOKEN:
270 |
271 | return 'https://pep.shaparak.ir/Api/v1/Payment/GetToken';
272 |
273 | case self::URL_CHECK:
274 |
275 | return 'https://pep.shaparak.ir/Api/v1/Payment/CheckTransactionResult';
276 |
277 | case self::URL_VERIFY:
278 |
279 | return 'https://pep.shaparak.ir/Api/v1/Payment/VerifyPayment';
280 |
281 | case self::URL_REFUND:
282 |
283 | return 'https://pep.shaparak.ir/Api/v1/Payment/RefundPayment';
284 |
285 | case self::UPDATE_SUBPAYMENT:
286 |
287 | return 'https://pep.shaparak.ir/Api/v1/Payment/UpdateInvoiceSubPayment';
288 |
289 | case self::GET_SUBPAYMENT:
290 |
291 | return 'https://pep.shaparak.ir/Api/v1/Payment/GetSubPaymentsReport';
292 |
293 | }
294 | } else {
295 | switch ($action) {
296 | case self::URL_GATEWAY:
297 |
298 | return $this->bankTestBaseUrl.'/pasargad/pep.shaparak.ir/gateway.aspx';
299 |
300 | case self::URL_TOKEN:
301 |
302 | return $this->bankTestBaseUrl.'/pasargad/pep.shaparak.ir/Api/v1/Payment/GetToken';
303 |
304 | case self::URL_CHECK:
305 |
306 | return $this->bankTestBaseUrl.'/pasargad/pep.shaparak.ir/Api/v1/Payment/CheckTransactionResult';
307 |
308 | case self::URL_VERIFY:
309 |
310 | return $this->bankTestBaseUrl.'/pasargad/pep.shaparak.ir/Api/v1/Payment/VerifyPayment';
311 |
312 | case self::URL_REFUND:
313 |
314 | return $this->bankTestBaseUrl.'/pasargad/pep.shaparak.ir/Api/v1/Payment/RefundPayment';
315 |
316 | case self::UPDATE_SUBPAYMENT:
317 |
318 | return $this->bankTestBaseUrl.'/pasargad/pep.shaparak.ir/Api/v1/Payment/UpdateInvoiceSubPayment';
319 |
320 | case self::GET_SUBPAYMENT:
321 |
322 | return $this->bankTestBaseUrl.'/pasargad/pep.shaparak.ir/Api/v1/Payment/GetSubPaymentsReport';
323 |
324 | }
325 | }
326 | throw new Exception("could not find url for {$action} action");
327 | }
328 | }
329 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shaparak :: Laravel Online Payment Component
2 | Online Payment Component for Laravel 5+ known as Shaparak component completely compatible with [BankTest](http://banktest.ir) simulator.
3 | Shaparak integrated all Iranian/Shetab payment gateways to one component.
4 |
5 | ## What is Banktest?
6 | - [BankTest](http://banktest.ir) is a sandbox service for all Iranian online payment gateways
7 | - [بانک تست](http://banktest.ir) یک سرویس شبیه ساز درگاه های پرداخت آنلاین ایرانی برای اهداف توسعه و تست نرم افزار می باشد
8 |
9 | ## Support This Project
10 |
11 | Please support the package by giving it :star: and contributing to its development.
12 |
13 | ## Currently supported PSPs:
14 |
15 | - Mellat Bank Gateway - درگاه بانک ملت (به پرداخت ملت) لاراول
16 | - Saman Bank Gateway - درگاه بانک سامان (پرداخت الکترونیک سامان) لاراول
17 | - Saderat Bank Gateway - درگاه بانک صادرات (پرداخت الکترونیک سپهر) لاراول
18 | - Pasargad Bank Gateway - درگاه بانک پاسارگاد (پرداخت الکترونیک پاسارگاد) لاراول
19 | - Parsian Bank Gateway - درگاه بانک پارسیان (تجارت الکترونیک پارسیان) لاراول
20 | - Melli Bank Gateway - درگاه بانک ملی (سداد) لاراول
21 | - ...
22 |
23 | ## Requirements
24 | Shaparak require PHP 7.1+
25 |
26 | ## Installation
27 | 1. Installation via php composer
28 |
29 | ```bash
30 | composer require php-monsters/shaparak
31 | ```
32 | 2. Add package service provider to your app service providers:
33 |
34 | ```php
35 | PhpMonsters\Shaparak\ShaparakServiceProvider::class,
36 | ```
37 | 3. Add package alias to your app aliases:
38 |
39 | ```php
40 | 'Shaparak' => PhpMonsters\Shaparak\Facades\Shaparak::class,
41 | ```
42 | 4. Publish package assets and configs
43 |
44 | ```bash
45 | php artisan vendor:publish --provider="PhpMonsters\Shaparak\ShaparakServiceProvider"
46 | ```
47 |
48 | ## Configuration
49 | If you complete installation step correctly, you can find Shaparak config file as `shaparak.php` in you project config file.
50 |
51 | For using sandbox environment you should set ```SHAPARAK_MODE=development``` in your .env file otherwise set ```SHAPARAK_MODE=production```
52 |
53 | if you choose development mode, Shaparak uses [banktest.ir](https://banktest.ir) as its payment gateway.
54 |
55 |
56 | ## Usage
57 | ### Add required fields to the model migration
58 | ```php
59 | $table->string('token', 40)->nullable(); // It keeps token that we get from the IPG
60 | $table->jsonb('gateway_callback_params')->nullable(); // It keeps the IPG callback parameters (just for tracking and debugging)
61 |
62 | $table->boolean('verified')->default(false); // Transaction verified or not
63 | $table->boolean('after_verified')->default(false); // Transaction settled or not
64 | $table->boolean('reversed')->default(false); // Transaction revered/refunded or not
65 | $table->boolean('accomplished')->default(false); // Transaction accomplished or not
66 | ```
67 | ### Prepare required model(s)
68 | Your Transaction, Invoice or Order model MUST implement Shaparak Transaction Interface.
69 | ```php
70 | $this->token,
100 | ]
101 | ));
102 | }
103 |
104 | /**
105 | * set gateway token that fetched from ipg provider gateway
106 | */
107 | public function setGatewayToken(string $token, bool $save = true): bool
108 | {
109 | $this->token = $token;
110 | $this->status = (TransactionStatusEnum::GoneToGate)->value;
111 |
112 | if ($save) {
113 | return $this->save();
114 | }
115 |
116 | return true;
117 | }
118 |
119 | public function getGatewayToken(): string
120 | {
121 | return $this->token;
122 | }
123 |
124 | /**
125 | * check if you transaction is ready for requesting payment token
126 | */
127 | public function isReadyForTokenRequest(): bool
128 | {
129 | return intval($this->status) <= (TransactionStatusEnum::Callback)->value;
130 | }
131 |
132 | /**
133 | * check if transaction is ready for requesting verify transaction
134 | */
135 | public function isReadyForVerify(): bool
136 | {
137 | return intval($this->status) <= (TransactionStatusEnum::Verified)->value;
138 | }
139 |
140 | /**
141 | * check if transaction is ready for requesting inquiry transaction (if supported by gateway)
142 | */
143 | public function isReadyForInquiry(): bool
144 | {
145 | return intval($this->status) >= (TransactionStatusEnum::GoneToGate)->value;
146 | }
147 |
148 | /**
149 | * check if transaction is ready for requesting settle/... transaction (if needed by gateway)
150 | */
151 | public function isReadyForSettle(): bool
152 | {
153 | return intval($this->status) == (TransactionStatusEnum::Verified)->value;
154 | }
155 |
156 | /**
157 | * check if transaction is ready to mark as accomplished
158 | */
159 | public function isReadyForAccomplish(): bool
160 | {
161 | return (intval($this->status) >= (TransactionStatusEnum::Verified)->value) &&
162 | (intval($this->status) < (TransactionStatusEnum::Accomplished)->value);
163 | }
164 |
165 | public function ipgProviderSupportsRefund(): bool
166 | {
167 | return ! empty($this->providable) &&
168 | $this->providable->refund_support === true;
169 | }
170 |
171 | public function isReadyForReverse(): bool
172 | {
173 | return $this->status === TransactionStatusEnum::Callback->value;
174 | }
175 |
176 | public function isReadyForCancel(): bool
177 | {
178 | return $this->status === TransactionStatusEnum::Verified->value;
179 | }
180 |
181 | /**
182 | * check if transaction is ready for accomplishment (merchant verify)
183 | */
184 | public function isReadyForRefund(): bool
185 | {
186 | return
187 | $this->ipgProviderSupportsRefund() &&
188 | (int) $this->status !== (TransactionStatusEnum::Accomplished)->value;
189 | }
190 |
191 | /**
192 | * update transaction by paid card number (if provided by gateway)
193 | */
194 | public function setCardNumber(string $cardNumber, bool $save = true): bool
195 | {
196 | $this->cardNumber = $cardNumber;
197 |
198 | if ($save) {
199 | return $this->save();
200 | }
201 |
202 | return true;
203 | }
204 |
205 | /**
206 | * mark transaction as verified
207 | */
208 | public function setVerified(bool $save = true): bool
209 | {
210 | $this->status = (TransactionStatusEnum::Verified)->value;
211 |
212 | if ($save) {
213 | return $this->save();
214 | }
215 |
216 | return true;
217 | }
218 |
219 | /**
220 | * mark transaction as settled/...
221 | */
222 | public function setSettled(bool $save = true): bool
223 | {
224 | $this->status = (TransactionStatusEnum::Settled)->value;
225 |
226 | if ($save) {
227 | return $this->save();
228 | }
229 |
230 | return true;
231 | }
232 |
233 | /**
234 | * mark transaction as reversed
235 | */
236 | public function setRefunded(bool $save = true): bool
237 | {
238 | $this->status = (TransactionStatusEnum::Refund)->value;
239 |
240 | if ($save) {
241 | return $this->save();
242 | }
243 |
244 | return true;
245 | }
246 |
247 | /**
248 | * get transaction amount
249 | */
250 | public function getPayableAmount(): int
251 | {
252 | return $this->payable_amount;
253 | }
254 |
255 | /**
256 | * save ipg provider's gateway callback parameters into transaction
257 | */
258 | public function setCallBackParameters(array $parameters, bool $save = true): bool
259 | {
260 | $this->gateway_callback_params = $parameters;
261 |
262 | if ($save) {
263 | return $this->save();
264 | }
265 |
266 | return true;
267 | }
268 |
269 | /**
270 | * @return false|mixed|string
271 | */
272 | public function getCallbackParams(): mixed
273 | {
274 | return $this->gateway_callback_params;
275 | }
276 | }
277 | ```
278 |
279 | ### Initialize a Shaparak instance
280 | ```php
281 | // method I: init Shaparak by passing custom gateway options
282 | $gatewayProperties = [
283 | 'terminal_id' => 'X1A3B5CY-X1DT4Z',
284 | 'terminal_pass' => '12345',
285 | ];
286 | $shaparak = Shaparak::with($psp, $transaction, $gatewayProperties)
287 | ->setParameters($request->all());
288 |
289 | // method II: init shaparak by config based gateway options
290 | // if you don't pass the third item it will use gateway's options from `config/shaparak.php` config file
291 | $shaparak = Shaparak::with($psp, $transaction)
292 | ->setParameters($request->all());
293 | ```
294 |
295 | ### Create goto IPG form (Payment form)
296 | Create a form in order to go to payment gateway. This form is auto-submit by default
297 | ```php
298 | // create your transaction as you desired
299 | $transaction = new Transaction();
300 | $transaction->user_id = $user->id;
301 | // ...
302 | $transaction->ip_address = $request->getClientIp();
303 | $transaction->description = $request->input('description');
304 | $transaction->save();
305 |
306 | try {
307 | $form = Shaparak::with('saman', $transaction)->getForm();
308 | } catch (\Exception $e) {
309 | XLog::exception($e);
310 | flash(trans('gate.could_not_create_goto_bank_form'))->error();
311 | return redirect()->back();
312 | }
313 | ```
314 | Show the form in your blade like:
315 | ```php
316 | {!! $form !!}
317 | ```
318 |
319 | ### Callback URL/route on your application
320 | In your callback action create a Shaparak instance and handle the transaction
321 | ```php
322 | $shaparak = Shaparak::with('saman', $order)
323 | ->setParameters($request->all());
324 |
325 | if ($shaparak->canContinueWithCallbackParameters($request->all()) !== true) {
326 | flash(trans('gate.could_not_continue_because_of_callback_params'))->error();
327 | // do what you need
328 | }
329 |
330 | $shaparak->setCallBackParameters($request->all());
331 |
332 | $verifyResult = $shaparak->verifyTransaction($request->all());
333 |
334 | ```
335 |
336 | ### Next steps (optional steps)
337 | Use the following methods based on your needs
338 |
339 | ```php
340 | $inquiryResult = $shaparak->inquiryTransaction($request->all());
341 | $settleResult = $shaparak->settleTransaction($request->all());
342 | $refundResult = $shaparak->refundTransaction($request->all());
343 | ```
344 |
345 | ## Security
346 |
347 | If you discover any security related issues, please email aboozar.ghf@gmail.com instead of using the issue tracker.
348 |
349 | ## Team
350 |
351 | This component is developed by the following person(s) and a bunch of [awesome contributors](https://github.com/iamtartan/laravel-online-payment/graphs/contributors).
352 |
353 | [](https://github.com/iamtartan) | [](https://github.com/maryamnbyn)
354 | --- | --- |
355 | [Aboozar Ghaffari](https://github.com/iamtartan) | [Maryam Nabiyan](https://github.com/maryamnbyn)
356 |
357 |
358 | ## License
359 |
360 | The Laravel Online Payment Module is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)
--------------------------------------------------------------------------------
/src/Provider/MellatProvider.php:
--------------------------------------------------------------------------------
1 | getTransaction();
26 |
27 | if ($transaction->isReadyForTokenRequest() === false) {
28 | throw new Exception('transaction is not ready for requesting token from payment gateway');
29 | }
30 |
31 | $this->checkRequiredActionParameters([
32 | 'terminal_id',
33 | 'username',
34 | 'password',
35 | ]);
36 |
37 | $sendParams = [
38 | 'terminalId' => (int) $this->getParameters('terminal_id'),
39 | 'userName' => $this->getParameters('username'),
40 | 'userPassword' => $this->getParameters('password'),
41 | 'orderId' => $this->getGatewayOrderId(), // get it from Transaction
42 | 'amount' => $this->getAmount(),
43 | 'localDate' => $this->getParameters('local_date', date('Ymd')),
44 | 'localTime' => $this->getParameters('local_time', date('His')),
45 | 'additionalData' => (string) $this->getParameters('additional_data'),
46 | 'callBackUrl' => $this->getCallbackUrl(),
47 | 'payerId' => (int) $this->getParameters('payer_id'),
48 | 'user_mobile' => '98' . substr($this->getParameters('user_mobile'), -10)
49 | ];
50 |
51 | try {
52 | $soapClient = $this->getSoapClient(self::URL_TOKEN);
53 |
54 | if (config('shaparak.providers.mellat.cumulative')) {
55 | $response = $soapClient->bpCumulativeDynamicPayRequest($sendParams);
56 | } else {
57 | $response = $soapClient->bpPayRequest($sendParams);
58 | }
59 |
60 | if (isset($response->return)) {
61 | $response = explode(',', $response->return);
62 |
63 | if ((int) $response[0] === 0) {
64 | $this->getTransaction()->setGatewayToken($response[1]); // update transaction
65 |
66 | return $response[1];
67 | }
68 |
69 | throw new Exception(sprintf('shaparak::mellat.error_%s', $response[0]));
70 | }
71 |
72 | throw new Exception('shaparak::shaparak.token_failed');
73 | } catch (SoapFault $e) {
74 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
75 | }
76 | }
77 |
78 | /**
79 | * {@inheritDoc}
80 | *
81 | * @throws Exception
82 | */
83 | public function getFormParameters(): array
84 | {
85 | return [
86 | 'gateway' => 'mellat',
87 | 'method' => 'post',
88 | 'action' => $this->getUrlFor(self::URL_GATEWAY),
89 | 'parameters' => [
90 | 'RefId' => $this->requestToken(),
91 | 'MobileNo' => $this->getParameters('user_mobile'),
92 | ],
93 | ];
94 | }
95 |
96 | protected function getGatewayOrderIdFromCallBackParameters(): string
97 | {
98 | return (string) $this->getParameters('SaleOrderId');
99 | }
100 |
101 | protected function callbackAbuseCheckList(): void
102 | {
103 | if (!(
104 | (string)$this->getTransaction()->gateway_order_id === $this->getGatewayOrderIdFromCallBackParameters()
105 | && (int)$this->getParameters('FinalAmount') === $this->getTransaction()->getPayableAmount()
106 | && (string)$this->getParameters('refID') === $this->getTransaction()->getGatewayToken()
107 | )) {
108 | throw new Exception('shaparak::shaparak.could_not_pass_abuse_checklist');
109 | }
110 | }
111 |
112 | /**
113 | * {@inheritDoc}
114 | *
115 | * @throws Exception
116 | */
117 | public function verifyTransaction(): bool
118 | {
119 | if ($this->getTransaction()->isReadyForVerify() === false) {
120 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
121 | }
122 |
123 | $this->checkRequiredActionParameters([
124 | 'terminal_id',
125 | 'username',
126 | 'password',
127 | 'RefId',
128 | 'ResCode',
129 | 'SaleOrderId',
130 | 'SaleReferenceId',
131 | 'CardHolderInfo',
132 | 'CardHolderPan',
133 | ]);
134 |
135 | $this->callbackAbuseCheckList();
136 |
137 | try {
138 |
139 | $sendParams = [
140 | 'terminalId' => (int) $this->getParameters('terminal_id'),
141 | 'userName' => $this->getParameters('username'),
142 | 'userPassword' => $this->getParameters('password'),
143 | 'orderId' => (int) $this->getParameters('SaleOrderId'), // same as SaleOrderId
144 | 'saleOrderId' => (int) $this->getParameters('SaleOrderId'),
145 | 'saleReferenceId' => (int) $this->getParameters('SaleReferenceId'),
146 | ];
147 |
148 |
149 | $soapClient = $this->getSoapClient(self::URL_VERIFY);
150 |
151 | $response = $soapClient->bpVerifyRequest($sendParams);
152 |
153 | if (isset($response->return)) {
154 | if ((int) $response->return !== 0) {
155 | throw new Exception(sprintf('shaparak::mellat.error_%s', $response->return));
156 | }
157 |
158 | $this->getTransaction()->setCardNumber($this->getParameters('CardHolderPan'));
159 | $this->getTransaction()->setVerified(true); // save()
160 |
161 | return true;
162 | }
163 |
164 | throw new Exception('shaparak::shaparak.verify_failed');
165 | } catch (SoapFault $e) {
166 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
167 | }
168 | }
169 |
170 | /**
171 | * @return string
172 | *
173 | * @throws Exception
174 | */
175 | public function inquiryTransaction() :string
176 | {
177 | if ($this->getTransaction()->isReadyForInquiry() === false) {
178 | throw new Exception('shaparak::shaparak.could_not_inquiry_payment');
179 | }
180 |
181 | $this->checkRequiredActionParameters([
182 | 'terminal_id',
183 | 'username',
184 | 'password',
185 | 'RefId',
186 | 'ResCode',
187 | 'SaleOrderId',
188 | 'SaleReferenceId',
189 | 'CardHolderInfo',
190 | ]);
191 |
192 | $sendParams = [
193 | 'terminalId' => (int) $this->getParameters('terminal_id'),
194 | 'userName' => $this->getParameters('username'),
195 | 'userPassword' => $this->getParameters('password'),
196 | 'orderId' => (int) $this->getParameters('SaleOrderId'), // same as SaleOrderId
197 | 'saleOrderId' => (int) $this->getParameters('SaleOrderId'),
198 | 'saleReferenceId' => (int) $this->getParameters('SaleReferenceId'),
199 | ];
200 |
201 | try {
202 | $soapClient = $this->getSoapClient(self::URL_INQUIRY);
203 |
204 | $response = $soapClient->bpInquiryRequest($sendParams);
205 |
206 | if (isset($response->return)) {
207 | return $response->return;
208 | }
209 |
210 | throw new Exception('shaparak::shaparak.inquiry_failed');
211 | } catch (SoapFault $e) {
212 |
213 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
214 | }
215 | }
216 |
217 | /**
218 | * Send settle request
219 | *
220 | *
221 | * @throws Exception
222 | */
223 | public function settleTransaction(): bool
224 | {
225 | if ($this->getTransaction()->isReadyForSettle() === false) {
226 | throw new Exception('shaparak::shaparak.could_not_settle_payment');
227 | }
228 |
229 | $this->checkRequiredActionParameters([
230 | 'terminal_id',
231 | 'username',
232 | 'password',
233 | 'RefId',
234 | 'ResCode',
235 | 'SaleOrderId',
236 | 'SaleReferenceId',
237 | 'CardHolderInfo',
238 | ]);
239 |
240 | $sendParams = [
241 | 'terminalId' => (int) $this->getParameters('terminal_id'),
242 | 'userName' => $this->getParameters('username'),
243 | 'userPassword' => $this->getParameters('password'),
244 | 'orderId' => (int) $this->getParameters('SaleOrderId'), // same as SaleOrderId
245 | 'saleOrderId' => (int) $this->getParameters('SaleOrderId'),
246 | 'saleReferenceId' => (int) $this->getParameters('SaleReferenceId'),
247 | ];
248 |
249 | try {
250 | $soapClient = $this->getSoapClient(self::URL_SETTLE);
251 |
252 | $response = $soapClient->bpSettleRequest($sendParams);
253 |
254 | if (isset($response->return) && is_numeric($response->return)) {
255 | if ((int) $response->return === 0 || (int) $response->return === 45) {
256 | $this->getTransaction()->setSettled(true);
257 |
258 | return true;
259 | }
260 |
261 | throw new Exception(sprintf('shaparak::mellat.error_%s', $response->return));
262 | }
263 |
264 | throw new Exception('shaparak::shaparak.invalid_response');
265 | } catch (SoapFault $e) {
266 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
267 | }
268 |
269 | }
270 |
271 | /**
272 | * {@inheritDoc}
273 | *
274 | * @throws Exception
275 | */
276 | public function refundTransaction(): bool
277 | {
278 | if ($this->getTransaction()->isReadyForRefund() === false) {
279 | throw new Exception('shaparak::shaparak.could_not_refund_payment');
280 | }
281 |
282 | $this->checkRequiredActionParameters([
283 | 'terminal_id',
284 | 'username',
285 | 'password',
286 | 'RefId',
287 | 'ResCode',
288 | 'SaleOrderId',
289 | 'SaleReferenceId',
290 | 'CardHolderInfo',
291 | ]);
292 |
293 | try {
294 | $sendParams = [
295 | 'terminalId' => (int) $this->getParameters('terminal_id'),
296 | 'userName' => $this->getParameters('username'),
297 | 'userPassword' => $this->getParameters('password'),
298 | 'orderId' => (int) $this->getParameters('SaleOrderId'), // same as SaleOrderId
299 | 'saleOrderId' => (int) $this->getParameters('SaleOrderId'),
300 | 'saleReferenceId' => (int) $this->getParameters('SaleReferenceId'),
301 | ];
302 |
303 | $soapClient = $this->getSoapClient(self::URL_REFUND);
304 |
305 | $response = $soapClient->bpReversalRequest($sendParams);
306 |
307 | if (isset($response->return) && is_numeric($response->return)) {
308 | if ((int) $response->return === 0 || (int) $response->return === 45) {
309 | $this->getTransaction()->setRefunded(true);
310 |
311 | return true;
312 | }
313 |
314 | throw new Exception('shaparak::mellat.error_'. $response->return);
315 | }
316 |
317 | throw new Exception('shaparak::mellat.errors.invalid_response');
318 | } catch (SoapFault $e) {
319 | throw new Exception('SoapFault: '.$e->getMessage().' #'.$e->getCode(), $e->getCode());
320 | }
321 | }
322 |
323 | /**
324 | * {@inheritDoc}
325 | */
326 | public function canContinueWithCallbackParameters(): bool
327 | {
328 | try {
329 | $this->checkRequiredActionParameters([
330 | 'RefId',
331 | 'ResCode',
332 | 'SaleOrderId',
333 | 'SaleReferenceId',
334 | 'CardHolderInfo',
335 | ]);
336 | } catch (\Exception $e) {
337 | return false;
338 | }
339 |
340 | if (! empty($this->getParameters('RefId'))) {
341 | return true;
342 | }
343 |
344 | return false;
345 | }
346 |
347 | /**
348 | * {@inheritDoc}
349 | *
350 | * @throws Exception
351 | */
352 | public function getGatewayReferenceId(): string
353 | {
354 | $this->checkRequiredActionParameters([
355 | 'RefId',
356 | ]);
357 |
358 | return $this->getParameters('RefId');
359 | }
360 |
361 | /**
362 | * {@inheritDoc}
363 | */
364 | public function getUrlFor(string $action): string
365 | {
366 | if ($this->environment === 'production') {
367 | return match ($action) {
368 | self::URL_GATEWAY => 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat',
369 | default => 'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl',
370 | };
371 | }
372 |
373 | return match ($action) {
374 | self::URL_GATEWAY => $this->bankTestBaseUrl . '/mellat/bpm.shaparak.ir/pgwchannel/startpay.mellat',
375 | default => $this->bankTestBaseUrl . '/mellat/bpm.shaparak.ir/pgwchannel/services/pgw?wsdl',
376 | };
377 | }
378 | }
379 |
--------------------------------------------------------------------------------
/src/Provider/AsanPardakhtProvider.php:
--------------------------------------------------------------------------------
1 | 'asanpardakht',
35 | 'method' => self::POST_METHOD,
36 | 'action' => $this->getUrlFor(self::URL_GATEWAY),
37 | 'parameters' => [
38 | 'RefId' => $this->requestToken(),
39 | 'mobileap' => $this->getParameters('user_mobile'),
40 | ],
41 | ];
42 | }
43 |
44 | /**
45 | * {@inheritDoc}
46 | */
47 | public function getUrlFor(string $action): string
48 | {
49 | if ($this->environment === 'production') {
50 | switch ($action) {
51 | case self::URL_GATEWAY:
52 |
53 | return 'https://asan.shaparak.ir';
54 |
55 | case self::URL_TOKEN:
56 |
57 | return 'https://ipgrest.asanpardakht.ir/v1/Token';
58 |
59 | case self::URL_VERIFY:
60 |
61 | return 'https://ipgrest.asanpardakht.ir/v1/Verify';
62 |
63 | case self::URL_RESULT:
64 |
65 | return 'https://ipgrest.asanpardakht.ir/v1/TranResult';
66 |
67 | case self::URL_SETTLEMENT:
68 |
69 | return 'https://ipgrest.asanpardakht.ir/v1/Settlement';
70 |
71 | case self::URL_REFUND:
72 |
73 | return 'https://ipgrest.asanpardakht.ir/v1/Reverse';
74 |
75 | case self::URL_CANCEL:
76 |
77 | return 'https://ipgrest.asanpardakht.ir/v1/Cancel';
78 |
79 | }
80 | } else {
81 | switch ($action) {
82 | case self::URL_GATEWAY:
83 |
84 | return $this->bankTestBaseUrl.'/ap/asan.shaparak.ir';
85 |
86 | case self::URL_TOKEN:
87 |
88 | return $this->bankTestBaseUrl.'/ap/ipgrest.asanpardakht.ir/v1/Token';
89 |
90 | case self::URL_VERIFY:
91 |
92 | return $this->bankTestBaseUrl.'/ap/ipgrest.asanpardakht.ir/v1/Verify';
93 |
94 | case self::URL_RESULT:
95 |
96 | return $this->bankTestBaseUrl.'/ap/ipgrest.asanpardakht.ir/v1/TranResult';
97 |
98 | case self::URL_SETTLEMENT:
99 |
100 | return $this->bankTestBaseUrl.'/ap/ipgrest.asanpardakht.ir/v1/Settlement';
101 |
102 | case self::URL_REFUND:
103 |
104 | return $this->bankTestBaseUrl.'/ap/ipgrest.asanpardakht.ir/v1/Reverse';
105 |
106 | case self::URL_CANCEL:
107 |
108 | return $this->bankTestBaseUrl.'/ap/ipgrest.asanpardakht.ir/v1/Cancel';
109 |
110 | }
111 | }
112 | throw new Exception("could not find url for {$action} action");
113 | }
114 |
115 | /**
116 | * @throws Exception
117 | * @throws RequestTokenException|JsonException
118 | */
119 | public function requestToken(): string
120 | {
121 | $transaction = $this->getTransaction();
122 |
123 | if ($transaction->isReadyForTokenRequest() === false) {
124 | throw new RequestTokenException(
125 | 'transaction is not ready for requesting token from payment gateway'
126 | );
127 | }
128 |
129 | $response = $this->sendParamToAp(
130 | $this->requestTokenData(),
131 | $this->getUrlFor(self::URL_TOKEN),
132 | self::POST_METHOD
133 | );
134 |
135 | if ($response->successful() && ! empty($response->body())) {
136 | $this->getTransaction()->setGatewayToken(
137 | json_decode($response->body(), false, 512, JSON_THROW_ON_ERROR),
138 | true
139 | );
140 |
141 | return json_decode($response->body(), false, 512, JSON_THROW_ON_ERROR);
142 | }
143 |
144 | if ($response->status() !== 200) {
145 | //todo: handle error page
146 | throw new Exception(sprintf('shaparak::asanpardakht.error_%s', $response->status()));
147 | }
148 |
149 | throw new Exception('shaparak::shaparak.token_failed');
150 | }
151 |
152 | /**
153 | * @throws Exception
154 | */
155 | public function requestTokenData(): array
156 | {
157 | $this->checkRequiredActionParameters([
158 | 'terminal_id',
159 | 'username',
160 | 'password',
161 | 'local_date',
162 | 'local_time',
163 | ]);
164 |
165 | $data = [
166 | 'serviceTypeId' => 1,
167 | 'merchantConfigurationId' => $this->getParameters('terminal_id'),
168 | 'localInvoiceId' => $this->getGatewayOrderId(), // get it from Transaction
169 | 'amountInRials' => $this->getAmount(),
170 | 'localDate' => $this->getParameters('local_date').' '.$this->getParameters('local_time'),
171 | 'callbackURL' => $this->getCallbackUrl(),
172 | 'paymentId' => $this->getParameters('payment_id', 0),
173 | 'additionalData' => (string) $this->getParameters('additional_data', ''),
174 | ];
175 |
176 | return array_merge($data, $this->cumulativeData());
177 | }
178 |
179 | public function sendParamToAp(array $params, string $url, string $method): mixed
180 | {
181 | return Http::acceptJson()->withHeaders([
182 | 'usr' => $this->getParameters('username'),
183 | 'pwd' => $this->getParameters('password'),
184 | ])->withOptions([
185 | 'timeout' => 15,
186 | ])->$method(
187 | $url,
188 | $params
189 | );
190 | }
191 |
192 | protected function getGatewayOrderIdFromCallBackParameters(): string
193 | {
194 | return (string) $this->getTransaction()->getFromJsonb(key: 'salesOrderID', fieldName: 'gateway_callback_params');
195 | }
196 |
197 | protected function callbackAbuseCheckList(): void
198 | {
199 | if (!(
200 | $this->getGatewayOrderIdFromCallBackParameters() === (string)$this->getTransaction()->gateway_order_id
201 | && (int)$this->getTransaction()->getFromJsonb(key: 'amount', fieldName: 'gateway_callback_params') === $this->getTransaction()->getPayableAmount()
202 | && (string)$this->getTransaction()->getFromJsonb(key: 'refID', fieldName: 'gateway_callback_params') === $this->getTransaction()->getGatewayToken()
203 | )) {
204 | throw new Exception('shaparak::shaparak.could_not_pass_abuse_checklist');
205 | }
206 | }
207 |
208 | /**
209 | * {@inheritDoc}
210 | */
211 | public function verifyTransaction(): bool
212 | {
213 | // required parameters will be checked by getTransactionResult method
214 |
215 | if ($this->getTransaction()->isReadyForVerify() === false) {
216 | throw new Exception('shaparak::shaparak.could_not_verify_transaction');
217 | }
218 |
219 | $this->checkRequiredActionParameters([
220 | 'terminal_id',
221 | ]);
222 |
223 | $this->callbackAbuseCheckList();
224 |
225 | try {
226 | $response = $this->generateComplementaryOperation(self::URL_VERIFY);
227 |
228 | if ($response !== true) {
229 | return false;
230 | }
231 | $this->getTransaction()->setVerified(true);
232 |
233 | return true;
234 | } catch (\Exception $e) {
235 | $this->log('verifyTransaction: '.$e->getMessage().' #'.$e->getCode(), [], 'error');
236 |
237 | return false;
238 | }
239 | }
240 |
241 | /**
242 | * @throws Exception
243 | */
244 | public function getTransactionResult(): bool
245 | {
246 | $this->checkRequiredActionParameters([
247 | 'username',
248 | 'password',
249 | 'terminal_id',
250 | ]);
251 | $response = $this->sendParamToAp([
252 | 'localInvoiceId' => $this->getGatewayOrderId(),
253 | 'merchantConfigurationId' => $this->getParameters('terminal_id'),
254 | ], $this->getUrlFor(self::URL_RESULT), self::GET_METHOD);
255 |
256 | if ($response->successful() && $response->status() === 200 && ! empty($response->body())) {
257 | $this->getTransaction()->setCallBackParameters($response->json());
258 |
259 | return true;
260 | }
261 |
262 | return false;
263 | }
264 |
265 | /**
266 | * Send settle request
267 | *
268 | *
269 | * @throws Exception
270 | * @throws SettlementException
271 | */
272 | public function settleTransaction(): bool
273 | {
274 | $this->checkRequiredActionParameters([
275 | 'username',
276 | 'password',
277 | 'terminal_id',
278 | ]);
279 |
280 | if ($this->getTransaction()->isReadyForSettle() === false) {
281 | throw new SettlementException('shaparak::shaparak.could_not_settle_payment');
282 | }
283 |
284 | try {
285 | $response = $this->generateComplementaryOperation(self::URL_SETTLEMENT);
286 |
287 | if ($response !== true) {
288 | throw new Exception('shaparak::asanpardakht.could_not_settlement_transaction');
289 | }
290 |
291 | $this->getTransaction()->setSettled(true);
292 |
293 | return true;
294 | } catch (\Exception $e) {
295 | $this->log($e->getMessage(), [], 'error');
296 | throw new SettlementException(
297 | 'settleTransaction: '.$e->getMessage().' #'.$e->getCode(),
298 | $e->getCode()
299 | );
300 | }
301 | }
302 |
303 | /**
304 | * {@inheritDoc}
305 | *
306 | * @throws Exception
307 | * @throws RefundException
308 | */
309 | public function refundTransaction(): bool
310 | {
311 | $this->checkRequiredActionParameters([
312 | 'username',
313 | 'password',
314 | 'terminal_id',
315 | ]);
316 | if ($this->getTransaction()->isReadyForRefund() === false) {
317 | throw new RefundException(__('shaparak::shaparak.could_not_refund_payment'));
318 | }
319 |
320 | try {
321 | if ($this->getTransaction()->isReadyForReverse()) {
322 | $response = $this->generateComplementaryOperation(self::URL_REFUND);
323 | } elseif ($this->getTransaction()->isReadyForCancel()) {
324 | $response = $this->generateComplementaryOperation(self::URL_CANCEL);
325 | } else {
326 | throw new RefundException(__('shaparak::shaparak.could_not_refund_payment'));
327 | }
328 |
329 | if ($response !== true) {
330 | throw new Exception(__('shaparak::shaparak.refund_unknown_error'));
331 | }
332 |
333 | $this->getTransaction()->setRefunded(true);
334 |
335 | return true;
336 | } catch (\Exception $e) {
337 | $this->log($e->getMessage(), [], 'error');
338 |
339 | throw new RefundException(
340 | 'refundTransaction: '.$e->getMessage().' #'.$e->getCode(),
341 | $e->getCode()
342 | );
343 | }
344 | }
345 |
346 | /**
347 | * {@inheritDoc}
348 | */
349 | public function canContinueWithCallbackParameters(): bool
350 | {
351 | try {
352 | $this->checkRequiredActionParameters([
353 | 'ReturningParams',
354 | ]);
355 | } catch (\Exception $e) {
356 | return false;
357 | }
358 |
359 | if (! empty($this->getParameters('ReturningParams'))) {
360 | return $this->getTransactionResult();
361 | }
362 |
363 | return false;
364 | }
365 |
366 | /**
367 | * {@inheritDoc}
368 | */
369 | public function getGatewayReferenceId(): string
370 | {
371 | $this->checkRequiredActionParameters([
372 | 'rrn',
373 | ]);
374 |
375 | return $this->getParameters('rrn');
376 | }
377 |
378 | /**
379 | * @throws Exception
380 | */
381 | protected function generateComplementaryOperation($method): bool
382 | {
383 | $response = $this->sendParamToAp(
384 | [
385 | 'payGateTranId' => $this->getTransaction()->getCallbackParams()['payGateTranID'],
386 | 'merchantConfigurationId' => $this->getParameters('terminal_id'),
387 | ],
388 | $this->getUrlFor($method),
389 | self::POST_METHOD
390 | );
391 | if ($response->successful()) {
392 | return true;
393 | }
394 |
395 | return false;
396 | }
397 |
398 | private function cumulativeData(): array
399 | {
400 | if ($this->getParameters('settlement_portions')) {
401 | return [
402 | 'settlementPortions' => $this->getParameters('settlement_portions')
403 | ];
404 | }
405 | return [];
406 | }
407 | }
408 |
--------------------------------------------------------------------------------