├── cover.png
├── .gitignore
├── views
├── fail.blade.php
├── success.blade.php
└── layout.blade.php
├── src
├── Exceptions
│ ├── PaymentRequestException.php
│ ├── RoutesNotDefinedException.php
│ ├── CardTokenInactiveException.php
│ ├── ApiCredentialsException.php
│ ├── UnsupportedCurrencyException.php
│ └── UnsupportedLanguageException.php
├── Requests
│ ├── GetBalance.php
│ ├── JustPay.php
│ ├── AddCard.php
│ ├── GetTransactionInfo.php
│ ├── Refund.php
│ ├── Commit.php
│ └── PayWithCard.php
├── Enums
│ ├── Language.php
│ ├── Method.php
│ ├── Currency.php
│ ├── SwitchableEnum.php
│ └── Status.php
├── Concerns
│ ├── PayRequest.php
│ ├── ApiRequest.php
│ └── PayRequestAttributes.php
├── Facades
│ └── Payze.php
├── Traits
│ ├── HasCards.php
│ └── HasTransactions.php
├── Events
│ ├── PayzeTransactionPaid.php
│ └── PayzeTransactionFailed.php
├── Observers
│ └── PayzeTransactionObserver.php
├── Objects
│ └── Split.php
├── Controllers
│ ├── PayzeController.php.stub
│ └── PayzeController.php
├── Models
│ ├── PayzeLog.php
│ ├── PayzeTransaction.php
│ └── PayzeCardToken.php
├── Console
│ └── Commands
│ │ └── UpdateIncompleteTransactions.php
├── PayzeServiceProvider.php
└── Payze.php
├── database
└── migrations
│ ├── create_payze_logs_table.php.stub
│ ├── add_transaction_id_to_payze_logs_table.php.stub
│ ├── add_default_and_details_columns_to_payze_card_tokens_table.php.stub
│ ├── create_payze_card_tokens_table.php.stub
│ └── create_payze_transactions_table.php.stub
├── composer.json
├── config
└── payze.php
├── CHANGELOG.md
└── README.md
/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/payzeio/laravel-payze/HEAD/cover.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | vendor
3 | tests/temp
4 | composer.lock
5 | phpunit.xml
6 | .env
7 | .phpunit.result.cache
8 | .php_cs.cache
9 |
--------------------------------------------------------------------------------
/views/fail.blade.php:
--------------------------------------------------------------------------------
1 | @extends('payze::layout')
2 |
3 | @section('status', 'fail')
4 | @section('title', 'Payment failed - Payze')
5 | @section('message', 'Payment failed. Please try again')
6 |
--------------------------------------------------------------------------------
/views/success.blade.php:
--------------------------------------------------------------------------------
1 | @extends('payze::layout')
2 |
3 | @section('status', 'success')
4 | @section('title', 'Payment completed - Payze')
5 | @section('message', 'Payment completed successfully')
6 |
--------------------------------------------------------------------------------
/src/Exceptions/PaymentRequestException.php:
--------------------------------------------------------------------------------
1 | morphMany(PayzeCardToken::class, 'model');
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Traits/HasTransactions.php:
--------------------------------------------------------------------------------
1 | morphMany(PayzeTransaction::class, 'model');
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Exceptions/UnsupportedCurrencyException.php:
--------------------------------------------------------------------------------
1 | message = sprintf('Unsupported Currency "%s"', $currency);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Exceptions/UnsupportedLanguageException.php:
--------------------------------------------------------------------------------
1 | message = sprintf('Unsupported Language "%s"', $language);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Requests/JustPay.php:
--------------------------------------------------------------------------------
1 | amount($amount);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Events/PayzeTransactionPaid.php:
--------------------------------------------------------------------------------
1 | transaction = $transaction;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Events/PayzeTransactionFailed.php:
--------------------------------------------------------------------------------
1 | transaction = $transaction;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Enums/SwitchableEnum.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->string('message')->nullable();
19 | $table->json('payload')->nullable();
20 | $table->timestamps();
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::dropIfExists(config('payze.logs_table'));
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "payzeio/laravel-payze",
3 | "description": "Payze.io Payment Integration for Laravel",
4 | "type": "library",
5 | "require": {
6 | "php": ">=7.4|^8.0|^8.1|^8.2",
7 | "ext-json": "*",
8 | "guzzlehttp/guzzle": "^6|^7",
9 | "illuminate/support": ">=5.3"
10 | },
11 | "license": "MIT",
12 | "authors": [
13 | {
14 | "name": "Levan Lotuashvili",
15 | "email": "l.lotuashvili@gmail.com"
16 | }
17 | ],
18 | "autoload": {
19 | "psr-4": {
20 | "PayzeIO\\LaravelPayze\\": "src"
21 | }
22 | },
23 | "extra": {
24 | "laravel": {
25 | "providers": [
26 | "PayzeIO\\LaravelPayze\\PayzeServiceProvider"
27 | ],
28 | "aliases": {
29 | "Payze": "PayzeIO\\LaravelPayze\\Facades\\Payze"
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Observers/PayzeTransactionObserver.php:
--------------------------------------------------------------------------------
1 | is_completed && !$transaction->getOriginal('is_completed');
17 |
18 | if ($completed && ($transaction->is_paid && !$transaction->getOriginal('is_paid'))) {
19 | event(new PayzeTransactionPaid($transaction));
20 | } elseif ($completed && (!$transaction->is_paid && !$transaction->getOriginal('is_paid'))) {
21 | event(new PayzeTransactionFailed($transaction));
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Concerns/ApiRequest.php:
--------------------------------------------------------------------------------
1 | method;
20 | }
21 |
22 | /**
23 | * @return array
24 | */
25 | public function toRequest(): array
26 | {
27 | return [];
28 | }
29 |
30 | /**
31 | * Return new instance
32 | *
33 | * @return static
34 | */
35 | public static function request(): self
36 | {
37 | return new static(...func_get_args());
38 | }
39 |
40 | /**
41 | * Process request via Payze facade
42 | *
43 | * @return mixed
44 | */
45 | public function process()
46 | {
47 | return Payze::process($this);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/database/migrations/add_transaction_id_to_payze_logs_table.php.stub:
--------------------------------------------------------------------------------
1 | foreignId('transaction_id')->nullable()->after('id')->constrained(config('payze.transactions_table'))->cascadeOnUpdate()->nullOnDelete();
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | *
24 | * @return void
25 | */
26 | public function down()
27 | {
28 | Schema::table(config('payze.logs_table'), function (Blueprint $table) {
29 | $table->dropConstrainedForeignId('transaction_id');
30 | });
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/Objects/Split.php:
--------------------------------------------------------------------------------
1 | amount = $amount;
38 | $this->iban = $iban;
39 | $this->payIn = $payIn;
40 | }
41 |
42 | /**
43 | * @return array
44 | */
45 | public function toRequest(): array
46 | {
47 | return [
48 | 'amount' => $this->amount,
49 | 'iban' => $this->iban,
50 | 'payIn' => $this->payIn,
51 | ];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Requests/AddCard.php:
--------------------------------------------------------------------------------
1 | amount($amount);
29 | }
30 |
31 | /**
32 | * Set the model for card token
33 | *
34 | * @param \Illuminate\Database\Eloquent\Model $model
35 | *
36 | * @return $this
37 | */
38 | public function assignTo(Model $model): self
39 | {
40 | $this->assignTo = $model;
41 |
42 | return $this;
43 | }
44 |
45 | /**
46 | * @return \Illuminate\Database\Eloquent\Model|null
47 | */
48 | public function getAssignedModel(): ?Model
49 | {
50 | return $this->assignTo;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/database/migrations/add_default_and_details_columns_to_payze_card_tokens_table.php.stub:
--------------------------------------------------------------------------------
1 | boolean('default')->default(false)->after('active');
18 | $table->string('cardholder')->nullable()->after('card_mask');
19 | $table->string('brand')->nullable()->after('cardholder');
20 | $table->date('expiration_date')->nullable()->after('brand');
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::table(config('payze.card_tokens_table'), function (Blueprint $table) {
32 | $table->dropColumn([
33 | 'default',
34 | 'cardholder',
35 | 'brand',
36 | 'expiration_date',
37 | ]);
38 | });
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/src/Requests/GetTransactionInfo.php:
--------------------------------------------------------------------------------
1 | transactionId = Payze::parseTransaction($transaction);
30 | }
31 |
32 | /**
33 | * Process request via Payze facade
34 | *
35 | * @return \PayzeIO\LaravelPayze\Models\PayzeTransaction
36 | */
37 | public function process(): PayzeTransaction
38 | {
39 | return Payze::processTransaction($this);
40 | }
41 |
42 | /**
43 | * @return array
44 | */
45 | public function toRequest(): array
46 | {
47 | return [
48 | 'transactionId' => $this->transactionId,
49 | ];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/views/layout.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | @yield('title')
8 |
9 |
10 |
11 |
12 |
39 |
40 |
41 |
42 | @yield('message')
43 |
44 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/database/migrations/create_payze_card_tokens_table.php.stub:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->bigInteger('transaction_id')->unsigned()->nullable();
19 | $table->foreign('transaction_id')
20 | ->references('id')
21 | ->on(config('payze.transactions_table'))
22 | ->onUpdate('cascade')
23 | ->onDelete('cascade');
24 | $table->bigInteger('model_id')->unsigned()->nullable();
25 | $table->string('model_type')->nullable();
26 | $table->boolean('active')->default(0);
27 | $table->string('card_mask')->nullable();
28 | $table->text('token');
29 | $table->timestamps();
30 |
31 | $table->index(['model_id', 'model_type']);
32 | });
33 | }
34 |
35 | /**
36 | * Reverse the migrations.
37 | *
38 | * @return void
39 | */
40 | public function down()
41 | {
42 | Schema::dropIfExists(config('payze.card_tokens_table'));
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/config/payze.php:
--------------------------------------------------------------------------------
1 | (bool) env('PAYZE_LOG', env('APP_ENV') === 'local'),
9 |
10 | /*
11 | * Enable/Disable SSL verification in Guzzle
12 | */
13 | 'verify_ssl' => (bool) env('PAYZE_VERIFY_SSL', true),
14 |
15 | /*
16 | * Success & Fail route names.
17 | * Update these if you have defined routes under name/namespace, for example "api.payze.succes"
18 | */
19 | 'routes' => [
20 | 'success' => 'payze.success',
21 | 'fail' => 'payze.fail',
22 | ],
23 |
24 | /*
25 | * Success & Fail view names.
26 | * Set names of success and fail views. Setting null will redirect to "/" by default
27 | */
28 | 'views' => [
29 | 'success' => 'payze::success',
30 | 'fail' => 'payze::fail',
31 | ],
32 |
33 | /*
34 | * Name on transactions table in database
35 | */
36 | 'transactions_table' => 'payze_transactions',
37 |
38 | /*
39 | * Name of logs table in database
40 | */
41 | 'logs_table' => 'payze_logs',
42 |
43 | /*
44 | * Name of card tokens table in database
45 | */
46 | 'card_tokens_table' => 'payze_card_tokens',
47 |
48 | /*
49 | * API key for Payze
50 | */
51 | 'api_key' => env('PAYZE_API_KEY'),
52 |
53 | /*
54 | * API secret for Payze
55 | */
56 | 'api_secret' => env('PAYZE_API_SECRET'),
57 |
58 | ];
59 |
--------------------------------------------------------------------------------
/src/Requests/Refund.php:
--------------------------------------------------------------------------------
1 | transactionId = Payze::parseTransaction($transaction);
36 | $this->amount = $amount;
37 | }
38 |
39 | /**
40 | * Process request via Payze facade
41 | *
42 | * @return \PayzeIO\LaravelPayze\Models\PayzeTransaction
43 | */
44 | public function process(): PayzeTransaction
45 | {
46 | return Payze::processTransaction($this, 'transaction');
47 | }
48 |
49 | /**
50 | * @return array
51 | */
52 | public function toRequest(): array
53 | {
54 | return array_filter([
55 | 'transactionId' => $this->transactionId,
56 | 'amount' => $this->amount ?: null,
57 | ]);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Enums/Status.php:
--------------------------------------------------------------------------------
1 | where('active', true)`
10 |
11 | After: `$query->where('expiration_date', '>=', Carbon::now())`
12 |
13 | Please refer to [this section](README.md#card-token-model) to read more details.
14 |
15 | ## v2.0.0
16 |
17 | - Bumped PHP to version 7.4, Added property types ([7ec95e2](https://github.com/payzeio/laravel-payze/commit/7ec95e29b5a7e220cdde68384dfaabf955f9c134))
18 | - Use `Relation::morphMap()` aliases for transactions and card tokens tables ([133dcab](https://github.com/payzeio/laravel-payze/commit/133dcab7e7526c1c99678e22eafa8f271caf744a))
19 | - Fixed split method ([0a7a719](https://github.com/payzeio/laravel-payze/commit/0a7a719cce6be862f73055107dc97e16cac02e64))
20 | - Minor bug fixes
21 |
22 | ## v1.9.0
23 |
24 | - Added Transaction ID column to PayzeLog model ([ee30e45](https://github.com/payzeio/laravel-payze/commit/ee30e45ce52bd20a6bfb4e70eee300eb8787a30d))
25 |
26 | ## v1.8.5
27 |
28 | - Added support of UZB language ([d6dd3a7](https://github.com/payzeio/laravel-payze/commit/d6dd3a7ba2a909e319fd77dc92f355e5497829a9))
29 |
30 | ## v1.8.3
31 |
32 | - Added support of UZS currency ([ee76cc5](https://github.com/payzeio/laravel-payze/commit/ee76cc5f8b26683f639586ed59f772cee6f39bcc))
33 |
--------------------------------------------------------------------------------
/src/Controllers/PayzeController.php.stub:
--------------------------------------------------------------------------------
1 | transactionId = Payze::parseTransaction($transaction);
36 | $this->amount($amount);
37 | }
38 |
39 | /**
40 | * @param float $amount
41 | *
42 | * @return $this
43 | */
44 | public function amount(float $amount): self
45 | {
46 | $this->amount = max($amount, 0);
47 |
48 | return $this;
49 | }
50 |
51 | /**
52 | * Process request via Payze facade
53 | *
54 | * @return \PayzeIO\LaravelPayze\Models\PayzeTransaction
55 | */
56 | public function process(): PayzeTransaction
57 | {
58 | return Payze::processTransaction($this, 'data');
59 | }
60 |
61 | public function toRequest(): array
62 | {
63 | return array_filter([
64 | 'transactionId' => $this->transactionId,
65 | 'amount' => $this->amount ?: null,
66 | ]);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Requests/PayWithCard.php:
--------------------------------------------------------------------------------
1 | active, new CardTokenInactiveException);
36 |
37 | $this->cardToken = $cardToken;
38 | $this->amount($amount);
39 | }
40 |
41 | /**
42 | * Process request via Payze facade
43 | *
44 | * @return \PayzeIO\LaravelPayze\Models\PayzeTransaction
45 | */
46 | public function process(): PayzeTransaction
47 | {
48 | return Payze::processTransaction($this, 'transactionInfo');
49 | }
50 |
51 | /**
52 | * @return array
53 | * @throws \PayzeIO\LaravelPayze\Exceptions\RoutesNotDefinedException
54 | * @throws \Throwable
55 | */
56 | public function toRequest(): array
57 | {
58 | return array_merge(parent::toRequest(), [
59 | 'cardToken' => $this->cardToken->getToken(),
60 | ]);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/database/migrations/create_payze_transactions_table.php.stub:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->bigInteger('model_id')->unsigned()->nullable();
19 | $table->string('model_type')->nullable();
20 | $table->string('method')->nullable();
21 | $table->string('status');
22 | $table->boolean('is_paid')->default(0);
23 | $table->boolean('is_completed')->default(0);
24 | $table->string('transaction_id')->unique()->index();
25 | $table->decimal('amount', 10);
26 | $table->decimal('final_amount', 10)->nullable();
27 | $table->decimal('refunded', 10)->nullable();
28 | $table->decimal('commission')->nullable();
29 | $table->boolean('refundable')->default(0);
30 | $table->string('currency');
31 | $table->string('lang');
32 | $table->json('split')->nullable();
33 | $table->boolean('can_be_committed')->default(0);
34 | $table->string('result_code')->nullable();
35 | $table->string('card_mask')->nullable();
36 | $table->json('log')->nullable();
37 | $table->timestamps();
38 |
39 | $table->index(['model_id', 'model_type']);
40 | });
41 | }
42 |
43 | /**
44 | * Reverse the migrations.
45 | *
46 | * @return void
47 | */
48 | public function down()
49 | {
50 | Schema::dropIfExists(config('payze.transactions_table'));
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/src/Models/PayzeLog.php:
--------------------------------------------------------------------------------
1 | 'array',
47 | ];
48 |
49 | /**
50 | * PayzeLog constructor.
51 | *
52 | * @param array $attributes
53 | */
54 | public function __construct(array $attributes = [])
55 | {
56 | parent::__construct($attributes);
57 |
58 | $this->table = config('payze.logs_table', 'payze_logs');
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Console/Commands/UpdateIncompleteTransactions.php:
--------------------------------------------------------------------------------
1 | where('created_at', '<=', now()->subMinutes(20))->get();
36 |
37 | if ($transactions->isEmpty()) {
38 | $this->info('All transactions are completed');
39 |
40 | return;
41 | }
42 |
43 | $this->info(sprintf('Updating %s transactions...', $transactions->count()));
44 |
45 | $transactions->each(function (PayzeTransaction $transaction) {
46 | try {
47 | $result = GetTransactionInfo::request($transaction)->process();
48 | } catch (Exception $e) {
49 | $this->error(sprintf('Can\'t update transaction (#%s) %s, setting status to Error.', $transaction->id, $transaction->transaction_id));
50 |
51 | $transaction->update([
52 | 'status' => Status::ERROR,
53 | 'is_completed' => true,
54 | ]);
55 |
56 | return;
57 | }
58 |
59 | $this->info(sprintf('%s - %s', $transaction->transaction_id, $result->status));
60 | });
61 |
62 | $this->info(sprintf('%s transactions updated successfully', $transactions->count()));
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/PayzeServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
16 | $this->publishes([
17 | __DIR__ . '/../database/migrations/create_payze_transactions_table.php.stub' => $this->getMigrationFileName('create_payze_transactions_table.php', '_01'),
18 | __DIR__ . '/../database/migrations/create_payze_logs_table.php.stub' => $this->getMigrationFileName('create_payze_logs_table.php', '_02'),
19 | __DIR__ . '/../database/migrations/create_payze_card_tokens_table.php.stub' => $this->getMigrationFileName('create_payze_card_tokens_table.php', '_03'),
20 | __DIR__ . '/../database/migrations/add_transaction_id_to_payze_logs_table.php.stub' => $this->getMigrationFileName('add_transaction_id_to_payze_logs_table.php', '_04'),
21 | __DIR__ . '/../database/migrations/add_default_and_details_columns_to_payze_card_tokens_table.php.stub' => $this->getMigrationFileName('add_default_and_details_columns_to_payze_card_tokens_table.php', '_05'),
22 | ], 'payze-migrations');
23 |
24 | $this->publishes([
25 | __DIR__ . '/../config/payze.php' => config_path('payze.php'),
26 | ], 'payze-config');
27 |
28 | $this->publishes([
29 | __DIR__ . '/Controllers/PayzeController.php.stub' => app_path('Http/Controllers/PayzeController.php'),
30 | ], 'payze-controllers');
31 | }
32 |
33 | $this->mergeConfigFrom(__DIR__ . '/../config/payze.php', 'payze');
34 |
35 | $this->loadViewsFrom(__DIR__ . '/../views', 'payze');
36 |
37 | PayzeTransaction::observe(PayzeTransactionObserver::class);
38 | }
39 |
40 | /**
41 | * Returns existing migration file if found, else uses the current timestamp.
42 | *
43 | * @param $migrationFileName
44 | * @param string $prefix
45 | *
46 | * @return string
47 | */
48 | protected function getMigrationFileName($migrationFileName, string $prefix = ''): string
49 | {
50 | $timestamp = date('Y_m_d_His') . $prefix;
51 |
52 | return Collection::make($this->app->databasePath() . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR)
53 | ->flatMap(fn($path) => File::glob($path . '*_' . $migrationFileName))
54 | ->push($this->app->databasePath() . "/migrations/{$timestamp}_$migrationFileName")
55 | ->first();
56 | }
57 |
58 | public function register()
59 | {
60 | $this->app->bind(Payze::class);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Controllers/PayzeController.php:
--------------------------------------------------------------------------------
1 | getTransaction($request);
29 |
30 | if (!$transaction->is_paid) {
31 | return redirect()->route(config('payze.routes.fail'), $request->query());
32 | }
33 |
34 | $response = $this->successResponse($transaction, $request);
35 |
36 | return $response ?? $this->response('success');
37 | }
38 |
39 | /**
40 | * Failed payment view
41 | *
42 | * @param \Illuminate\Http\Request $request
43 | *
44 | * @return mixed
45 | */
46 | public function fail(Request $request)
47 | {
48 | $transaction = $this->getTransaction($request);
49 |
50 | if ($transaction->is_paid) {
51 | return redirect()->route(config('payze.routes.success'), $request->query());
52 | }
53 |
54 | $response = $this->failResponse($transaction, $request);
55 |
56 | return $response ?? $this->response('fail');
57 | }
58 |
59 | /**
60 | * Update information in database and return transaction
61 | *
62 | * @param \Illuminate\Http\Request $request
63 | *
64 | * @return \PayzeIO\LaravelPayze\Models\PayzeTransaction
65 | */
66 | protected function getTransaction(Request $request): PayzeTransaction
67 | {
68 | abort_unless($request->has($this->key), 404);
69 |
70 | $id = $request->input($this->key);
71 |
72 | /*
73 | * Check if transaction is incomplete
74 | * Fixes security issue. Avoids triggering success callbacks on completed transactions more than once
75 | */
76 | PayzeTransaction::where('transaction_id', $id)->incomplete()->firstOrFail();
77 |
78 | return GetTransactionInfo::request($id)->process();
79 | }
80 |
81 | /**
82 | * Return a view or redirect to index page, based on config
83 | *
84 | * @param string $status
85 | *
86 | * @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
87 | */
88 | protected function response(string $status)
89 | {
90 | $view = config('payze.views.' . $status);
91 |
92 | return $view ? view($view) : redirect('/');
93 | }
94 |
95 | /**
96 | * Success Response
97 | * Should be overridden in custom controller, or will be used a default one
98 | *
99 | * @param \PayzeIO\LaravelPayze\Models\PayzeTransaction $transaction
100 | * @param \Illuminate\Http\Request $request
101 | *
102 | * @return mixed
103 | */
104 | protected function successResponse(PayzeTransaction $transaction, Request $request)
105 | {
106 | // Override in controller
107 | }
108 |
109 | /**
110 | * Fail Response
111 | * Should be overridden in custom controller, or will be used a default one
112 | *
113 | * @param \PayzeIO\LaravelPayze\Models\PayzeTransaction $transaction
114 | * @param \Illuminate\Http\Request $request
115 | *
116 | * @return mixed
117 | */
118 | protected function failResponse(PayzeTransaction $transaction, Request $request)
119 | {
120 | // Override in controller
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/Models/PayzeTransaction.php:
--------------------------------------------------------------------------------
1 | 'float',
107 | 'final_amount' => 'float',
108 | 'commission' => 'float',
109 | 'split' => 'array',
110 | 'can_be_committed' => 'boolean',
111 | 'refunded' => 'float',
112 | 'refundable' => 'boolean',
113 | 'log' => 'array',
114 | ];
115 |
116 | /**
117 | * PayzeTranscation constructor.
118 | *
119 | * @param array $attributes
120 | */
121 | public function __construct(array $attributes = [])
122 | {
123 | parent::__construct($attributes);
124 |
125 | $this->table = config('payze.transactions_table', 'payze_transactions');
126 | }
127 |
128 | /**
129 | * @return \Illuminate\Database\Eloquent\Relations\MorphTo
130 | */
131 | public function model(): MorphTo
132 | {
133 | return $this->morphTo();
134 | }
135 |
136 | /**
137 | * @return \Illuminate\Database\Eloquent\Relations\HasOne
138 | */
139 | public function card(): HasOne
140 | {
141 | return $this->hasOne(PayzeCardToken::class, 'transaction_id')->withInactive();
142 | }
143 |
144 | /**
145 | * @param \Illuminate\Database\Eloquent\Builder $query
146 | *
147 | * @return \Illuminate\Database\Eloquent\Builder
148 | */
149 | public function scopePaid(Builder $query): Builder
150 | {
151 | return $query->where('is_paid', true);
152 | }
153 |
154 | /**
155 | * @param \Illuminate\Database\Eloquent\Builder $query
156 | *
157 | * @return \Illuminate\Database\Eloquent\Builder
158 | */
159 | public function scopeUnpaid(Builder $query): Builder
160 | {
161 | return $query->where('is_paid', false);
162 | }
163 |
164 | /**
165 | * @param \Illuminate\Database\Eloquent\Builder $query
166 | *
167 | * @return \Illuminate\Database\Eloquent\Builder
168 | */
169 | public function scopeCompleted(Builder $query): Builder
170 | {
171 | return $query->where('is_completed', true);
172 | }
173 |
174 | /**
175 | * @param \Illuminate\Database\Eloquent\Builder $query
176 | *
177 | * @return \Illuminate\Database\Eloquent\Builder
178 | */
179 | public function scopeIncomplete(Builder $query): Builder
180 | {
181 | return $query->where('is_completed', false);
182 | }
183 |
184 | /**
185 | * @param \Illuminate\Database\Eloquent\Builder $query
186 | *
187 | * @return \Illuminate\Database\Eloquent\Builder
188 | */
189 | public function scopeRefundable(Builder $query): Builder
190 | {
191 | return $query->where('refundable', true);
192 | }
193 |
194 | /**
195 | * @param \Illuminate\Database\Eloquent\Builder $query
196 | *
197 | * @return \Illuminate\Database\Eloquent\Builder
198 | */
199 | public function scopeNonRefundable(Builder $query): Builder
200 | {
201 | return $query->where('refundable', false);
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/Models/PayzeCardToken.php:
--------------------------------------------------------------------------------
1 | 'boolean',
69 | 'default' => 'boolean',
70 | 'expiration_date' => 'datetime',
71 | ];
72 |
73 | /**
74 | * PayzeCardToken constructor.
75 | *
76 | * @param array $attributes
77 | */
78 | public function __construct(array $attributes = [])
79 | {
80 | parent::__construct($attributes);
81 |
82 | $this->table = config('payze.card_tokens_table', 'payze_card_tokens');
83 | }
84 |
85 | protected static function booted()
86 | {
87 | /*
88 | * Add global active scope
89 | */
90 | static::addGlobalScope('active', fn(Builder $builder) => $builder->where('active', true));
91 | }
92 |
93 | /**
94 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
95 | */
96 | public function transaction(): BelongsTo
97 | {
98 | return $this->belongsTo(PayzeTransaction::class, 'transaction_id');
99 | }
100 |
101 | /**
102 | * @return \Illuminate\Database\Eloquent\Relations\MorphTo
103 | */
104 | public function model(): MorphTo
105 | {
106 | return $this->morphTo();
107 | }
108 |
109 | /**
110 | * @param \Illuminate\Database\Eloquent\Builder $query
111 | *
112 | * @return \Illuminate\Database\Eloquent\Builder
113 | */
114 | public function scopeWithInactive(Builder $query): Builder
115 | {
116 | return $query->withoutGlobalScope('active');
117 | }
118 |
119 | /**
120 | * @param \Illuminate\Database\Eloquent\Builder $query
121 | *
122 | * @return \Illuminate\Database\Eloquent\Builder
123 | */
124 | public function scopeDefault(Builder $query): Builder
125 | {
126 | return $query->where('default', true);
127 | }
128 |
129 | /**
130 | * @param \Illuminate\Database\Eloquent\Builder $query
131 | *
132 | * @return \Illuminate\Database\Eloquent\Builder
133 | */
134 | public function scopeActive(Builder $query): Builder
135 | {
136 | return $query->where('expiration_date', '>=', Carbon::now());
137 | }
138 |
139 | /**
140 | * @param \Illuminate\Database\Eloquent\Builder $query
141 | *
142 | * @return \Illuminate\Database\Eloquent\Builder
143 | */
144 | public function scopeInactive(Builder $query): Builder
145 | {
146 | return $query->withInactive()->where('active', false);
147 | }
148 |
149 | /**
150 | * @param \Illuminate\Database\Eloquent\Builder $query
151 | *
152 | * @return \Illuminate\Database\Eloquent\Builder
153 | */
154 | public function scopeExpired(Builder $query): Builder
155 | {
156 | return $query->where('expiration_date', '<', Carbon::now());
157 | }
158 |
159 | /**
160 | * Encrypt a card token before saving to DB
161 | *
162 | * @param string $value
163 | */
164 | public function setTokenAttribute(string $value): void
165 | {
166 | if (empty($value)) {
167 | return;
168 | }
169 |
170 | $this->attributes['token'] = Crypt::encryptString($value);
171 | }
172 |
173 | /**
174 | * Return a decrypted token
175 | *
176 | * @return string|null
177 | */
178 | public function getToken(): string
179 | {
180 | return Crypt::decryptString($this->token);
181 | }
182 |
183 | /**
184 | * Mark current card as default
185 | * Unmark all other cards of the same model
186 | *
187 | * @return $this
188 | */
189 | public function markAsDefault(): self
190 | {
191 | self::where('model_type', $this->model_type)
192 | ->where('model_id', $this->model_id)
193 | ->where('id', '!=', $this->id)
194 | ->where('default', true)
195 | ->update(['default' => false]);
196 |
197 | return tap($this)->update([
198 | 'default' => true,
199 | ]);
200 | }
201 |
202 | /**
203 | * Check if current card is not expired
204 | *
205 | * @return bool
206 | */
207 | public function isActive(): bool
208 | {
209 | if (empty($this->expiration_date)) {
210 | return true;
211 | }
212 |
213 | return $this->expiration_date->isFuture();
214 | }
215 |
216 | /**
217 | * Check if current card is expired
218 | *
219 | * @return bool
220 | */
221 | public function isExpired(): bool
222 | {
223 | if (empty($this->expiration_date)) {
224 | return false;
225 | }
226 |
227 | return $this->expiration_date->isPast();
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/Concerns/PayRequestAttributes.php:
--------------------------------------------------------------------------------
1 | amount = max($amount, 0);
71 |
72 | return $this;
73 | }
74 |
75 | /**
76 | * Split transaction to different IBANs
77 | *
78 | * @param mixed $splits
79 | *
80 | * @return $this
81 | * @throws \PayzeIO\LaravelPayze\Exceptions\PaymentRequestException
82 | * @throws \Throwable
83 | */
84 | public function split($splits): self
85 | {
86 | $splits = !is_array($splits) ? func_get_args() : $splits;
87 |
88 | foreach ($splits as $split) {
89 | throw_unless(is_a($split, Split::class), new PaymentRequestException('Incorrect format. Please pass Split object'));
90 | }
91 |
92 | $this->split = $splits;
93 |
94 | return $this;
95 | }
96 |
97 | /**
98 | * Switch payment page language
99 | *
100 | * @param string $lang
101 | *
102 | * @return $this
103 | * @throws \PayzeIO\LaravelPayze\Exceptions\UnsupportedLanguageException|\Throwable
104 | */
105 | public function language(string $lang): self
106 | {
107 | throw_unless(Language::check($lang), new UnsupportedLanguageException($lang));
108 |
109 | $this->lang = $lang;
110 |
111 | return $this;
112 | }
113 |
114 | /**
115 | * Switch payment currency
116 | *
117 | * @param string $currency
118 | *
119 | * @return $this
120 | * @throws \PayzeIO\LaravelPayze\Exceptions\UnsupportedCurrencyException|\Throwable
121 | */
122 | public function currency(string $currency): self
123 | {
124 | $currency = strtoupper($currency);
125 |
126 | throw_unless(Currency::check($currency), new UnsupportedCurrencyException($currency));
127 |
128 | $this->currency = $currency;
129 |
130 | return $this;
131 | }
132 |
133 | /**
134 | * Set the model for transaction
135 | *
136 | * @param \Illuminate\Database\Eloquent\Model $model
137 | *
138 | * @return $this
139 | */
140 | public function for(Model $model): self
141 | {
142 | $this->model = $model;
143 |
144 | return $this;
145 | }
146 |
147 | /**
148 | * Set preauthorize option for transaction
149 | *
150 | * @param bool $preauthorize
151 | *
152 | * @return $this
153 | */
154 | public function preauthorize(bool $preauthorize = true): self
155 | {
156 | $this->preauthorize = $preauthorize;
157 |
158 | return $this;
159 | }
160 |
161 | /**
162 | * Set raw option for transaction
163 | *
164 | * @param bool $raw
165 | *
166 | * @return $this
167 | */
168 | public function raw(bool $raw = true): self
169 | {
170 | $this->raw = $raw;
171 |
172 | return $this;
173 | }
174 |
175 | /**
176 | * The user will be redirected to this URL, If the transaction is successful
177 | *
178 | * @param string $url
179 | *
180 | * @return $this
181 | */
182 | public function callback(string $url): self
183 | {
184 | $this->callback = $url;
185 |
186 | return $this;
187 | }
188 |
189 | /**
190 | * The user will be redirected to this URL, if the transaction fails
191 | *
192 | * @param string $url
193 | *
194 | * @return $this
195 | */
196 | public function callbackError(string $url): self
197 | {
198 | $this->callbackError = $url;
199 |
200 | return $this;
201 | }
202 |
203 | /**
204 | * @return float
205 | */
206 | public function getAmount(): float
207 | {
208 | return $this->amount;
209 | }
210 |
211 | /**
212 | * @return string
213 | */
214 | public function getCurrency(): string
215 | {
216 | return $this->currency;
217 | }
218 |
219 | /**
220 | * @return string
221 | */
222 | public function getLanguage(): string
223 | {
224 | return $this->lang;
225 | }
226 |
227 | /**
228 | * @return bool
229 | */
230 | public function getPreauthorize(): bool
231 | {
232 | return $this->preauthorize;
233 | }
234 |
235 | /**
236 | * @return bool
237 | */
238 | public function getRaw(): bool
239 | {
240 | return $this->raw;
241 | }
242 |
243 | /**
244 | * @return array
245 | */
246 | public function getSplit(): array
247 | {
248 | return $this->split;
249 | }
250 |
251 | /**
252 | * @return \Illuminate\Database\Eloquent\Model|null
253 | */
254 | public function getModel(): ?Model
255 | {
256 | return $this->model;
257 | }
258 |
259 | /**
260 | * @return array
261 | * @throws \PayzeIO\LaravelPayze\Exceptions\RoutesNotDefinedException
262 | * @throws \Throwable
263 | */
264 | public function toRequest(): array
265 | {
266 | $defaultRoutes = config('payze.routes');
267 |
268 | if ($this->callback) {
269 | $successName = $this->callback;
270 | } else {
271 | throw_if(empty($defaultRoutes['success'] ?? false) || !Route::has($defaultRoutes['success']), new RoutesNotDefinedException);
272 |
273 | $successName = route($defaultRoutes['success']);
274 | }
275 |
276 | if ($this->callbackError) {
277 | $failName = $this->callbackError;
278 | } else {
279 | throw_if(empty($defaultRoutes['fail'] ?? false) || !Route::has($defaultRoutes['fail']), new RoutesNotDefinedException);
280 |
281 | $failName = route($defaultRoutes['fail']);
282 | }
283 |
284 | return [
285 | 'amount' => $this->amount,
286 | 'currency' => $this->currency,
287 | 'lang' => $this->lang,
288 | 'preauthorize' => $this->preauthorize,
289 | 'callback' => $successName,
290 | 'callbackError' => $failName,
291 | 'split' => filled($this->split) ? array_map(fn(Split $split) => $split->toRequest(), $this->split) : [],
292 | ];
293 | }
294 |
295 | /**
296 | * @return array
297 | */
298 | public function toModel(): array
299 | {
300 | return [
301 | 'model_id' => !empty($this->model) ? $this->model->id : null,
302 | 'model_type' => !empty($this->model) ? Payze::modelType($this->model) : null,
303 | 'method' => $this->method,
304 | 'amount' => $this->amount,
305 | 'currency' => $this->currency,
306 | 'lang' => $this->lang,
307 | 'split' => filled($this->split) ? array_map(fn(Split $split) => $split->toRequest(), $this->split) : [],
308 | ];
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/src/Payze.php:
--------------------------------------------------------------------------------
1 | getMethod();
41 | $data = $request->toRequest();
42 |
43 | $this->log("Starting [$method] payment.", compact('method', 'data'));
44 | $response = $this->request($method, $data)['response'];
45 |
46 | $url = $response['transactionUrl'];
47 | $id = $response['transactionId'];
48 |
49 | throw_if(empty($id) || empty($url), new PaymentRequestException('Transaction ID is missing'));
50 |
51 | $this->log(
52 | "Transaction [$id] created",
53 | compact('id', 'response'),
54 | $transaction = $this->logTransaction($response ?? [], $request)
55 | );
56 |
57 | if ($method === Method::ADD_CARD && $request instanceof AddCard && filled($response['cardId'] ?? false)) {
58 | $this->saveCard($response['cardId'], $transaction, $request->getAssignedModel());
59 | }
60 |
61 | return $request->getRaw() ? $response : redirect($url);
62 | }
63 |
64 | /**
65 | * @param \PayzeIO\LaravelPayze\Concerns\ApiRequest $request
66 | * @param string|null $key
67 | *
68 | * @return \PayzeIO\LaravelPayze\Models\PayzeTransaction
69 | * @throws \GuzzleHttp\Exception\GuzzleException
70 | * @throws \PayzeIO\LaravelPayze\Exceptions\ApiCredentialsException
71 | * @throws \PayzeIO\LaravelPayze\Exceptions\PaymentRequestException
72 | * @throws \Throwable
73 | */
74 | public function processTransaction(ApiRequest $request, string $key = null): PayzeTransaction
75 | {
76 | $response = $this->process($request);
77 |
78 | if ($key) {
79 | $response = $response[$key];
80 | }
81 |
82 | return Payze::logTransaction($response, is_a($request, PayRequestAttributes::class) ? $request : null);
83 | }
84 |
85 | /**
86 | * @param \PayzeIO\LaravelPayze\Concerns\ApiRequest $request
87 | * @param bool $raw
88 | *
89 | * @return array
90 | * @throws \GuzzleHttp\Exception\GuzzleException
91 | * @throws \PayzeIO\LaravelPayze\Exceptions\ApiCredentialsException
92 | * @throws \PayzeIO\LaravelPayze\Exceptions\PaymentRequestException
93 | * @throws \Throwable
94 | */
95 | public function process(ApiRequest $request, bool $raw = false): array
96 | {
97 | $method = $request->getMethod();
98 | $data = $request->toRequest();
99 |
100 | $this->log("Sending [$method] request", $data);
101 |
102 | $response = $this->request($method, $data);
103 |
104 | $this->log("Received [$method] response", $response);
105 |
106 | return $raw ? $response : $response['response'];
107 | }
108 |
109 | /**
110 | * @param string $token
111 | * @param \PayzeIO\LaravelPayze\Models\PayzeTransaction $transaction
112 | * @param \Illuminate\Database\Eloquent\Model|null $model
113 | *
114 | * @return \PayzeIO\LaravelPayze\Models\PayzeCardToken
115 | */
116 | protected function saveCard(string $token, PayzeTransaction $transaction, ?Model $model = null): PayzeCardToken
117 | {
118 | return PayzeCardToken::create([
119 | 'token' => $token,
120 | 'transaction_id' => $transaction->id,
121 | 'model_id' => optional($model)->id ?? $transaction->model_id,
122 | 'model_type' => filled($model) ? Payze::modelType($model) : $transaction->model_type,
123 | ]);
124 | }
125 |
126 | /**
127 | * Create/update transaction entry in database
128 | *
129 | * @param array $data
130 | * @param \PayzeIO\LaravelPayze\Concerns\PayRequestAttributes|null $request
131 | *
132 | * @return \PayzeIO\LaravelPayze\Models\PayzeTransaction
133 | */
134 | public function logTransaction(array $data, PayRequestAttributes $request = null): PayzeTransaction
135 | {
136 | return tap(PayzeTransaction::firstOrNew([
137 | 'transaction_id' => $data['transactionId'],
138 | ]), function (PayzeTransaction $transaction) use ($data, $request) {
139 | $transaction->fill(array_merge($request ? $request->toModel() : [], array_filter([
140 | 'split' => $data['split'] ?? null,
141 | 'status' => $status = $data['status'] ?? Status::CREATED,
142 | 'is_paid' => Status::isPaid($status),
143 | 'is_completed' => Status::isCompleted($status),
144 | 'commission' => $data['commission'] ?? null,
145 | 'final_amount' => $data['finalAmount'] ?? null,
146 | 'can_be_committed' => $data['canBeCommitted'] ?? $data['getCanBeCommitted'] ?? false,
147 | 'refunded' => $data['refunded'] ?? null,
148 | 'refundable' => $data['refundable'] ?? false,
149 | 'card_mask' => $data['cardMask'] ?? null,
150 | 'result_code' => $data['resultCode'] ?? null,
151 | 'log' => $data['log'] ?? null,
152 | ])));
153 |
154 | foreach (['amount', 'currency'] as $field) {
155 | if (empty($transaction->$field) && !empty($data[$field])) {
156 | $transaction->$field = $data[$field];
157 | }
158 | }
159 |
160 | if (empty($transaction->lang)) {
161 | $transaction->lang = Language::DEFAULT;
162 | }
163 |
164 | $transaction->save();
165 |
166 | if ($transaction->is_paid) {
167 | $card = $transaction->card;
168 |
169 | if ($card && !$card->active) {
170 | $card->update([
171 | 'active' => true,
172 | 'default' => PayzeCardToken::where('model_type', $card->model_type)->where('model_id', $card->model_id)->where('default', true)->doesntExist(),
173 | 'card_mask' => $transaction->card_mask,
174 | 'cardholder' => $data['cardholder'] ?? null,
175 | 'brand' => $data['cardBrand'] ?? null,
176 | 'expiration_date' => $this->parseExpirationDate($data['expirationDate'] ?? null),
177 | ]);
178 | }
179 | }
180 | });
181 | }
182 |
183 | protected function parseExpirationDate(?string $date): ?Carbon
184 | {
185 | if (empty($date)) {
186 | return null;
187 | }
188 |
189 | $month = substr($date, 0, 2);
190 | $year = substr($date, -2);
191 |
192 | // Some merchants may receive an expiration date in reversed order
193 | // Month is always less than a year, so we can easily detect it and reverse
194 | if (intval($month) > intval($year)) {
195 | [$year, $month] = [$month, $year];
196 | }
197 |
198 | return Carbon::createFromFormat('dmy', '01' . $month . $year)->endOfMonth();
199 | }
200 |
201 | /**
202 | * Create log entry in database
203 | *
204 | * @param string $message
205 | * @param array $data
206 | * @param \PayzeIO\LaravelPayze\Models\PayzeTransaction|null $transaction
207 | */
208 | public function log(string $message, array $data = [], PayzeTransaction $transaction = null): void
209 | {
210 | if (!config('payze.log')) {
211 | return;
212 | }
213 |
214 | PayzeLog::create([
215 | 'transaction_id' => optional($transaction)->id,
216 | 'message' => $message,
217 | 'payload' => $data,
218 | ]);
219 | }
220 |
221 | /**
222 | * Send API request
223 | *
224 | * @param string $method
225 | * @param array $data
226 | *
227 | * @return array
228 | * @throws \GuzzleHttp\Exception\GuzzleException
229 | * @throws \PayzeIO\LaravelPayze\Exceptions\PaymentRequestException
230 | * @throws \PayzeIO\LaravelPayze\Exceptions\ApiCredentialsException
231 | * @throws \Throwable
232 | */
233 | public function request(string $method, array $data = []): array
234 | {
235 | $key = config('payze.api_key');
236 | $secret = config('payze.api_secret');
237 |
238 | throw_if(empty($key) || empty($secret), new ApiCredentialsException);
239 |
240 | try {
241 | $response = json_decode((new Client)->post('https://payze.io/api/v1', [
242 | 'headers' => [
243 | 'Content-Type' => 'application/json',
244 | 'user-agent' => 'laravel-payze',
245 | ],
246 | 'json' => [
247 | 'method' => $method,
248 | 'apiKey' => $key,
249 | 'apiSecret' => $secret,
250 | 'data' => $data ?: new stdClass,
251 | ],
252 | 'verify' => config('payze.verify_ssl', true),
253 | ])->getBody()->getContents(), true);
254 | } catch (RequestException $exception) {
255 | throw new PaymentRequestException($exception->getMessage());
256 | }
257 |
258 | throw_unless(empty($response['response']['error']), new PaymentRequestException($response['response']['error'] ?? 'Error'));
259 |
260 | return $response;
261 | }
262 |
263 | /**
264 | * Get a model type regarding relation morph map
265 | *
266 | * @param \Illuminate\Database\Eloquent\Model $model
267 | *
268 | * @return string
269 | */
270 | public static function modelType(Model $model): string
271 | {
272 | $morphMap = array_flip(Relation::morphMap());
273 | $class = get_class($model);
274 |
275 | return Arr::get($morphMap, $class, $class);
276 | }
277 |
278 | /**
279 | * @param $transaction
280 | *
281 | * @return string
282 | * @throws \PayzeIO\LaravelPayze\Exceptions\PaymentRequestException
283 | * @throws \Throwable
284 | */
285 | public static function parseTransaction($transaction): string
286 | {
287 | $isString = is_string($transaction) && filled($transaction);
288 | $isTransaction = is_a($transaction, PayzeTransaction::class);
289 |
290 | throw_unless($isString || $isTransaction, new PaymentRequestException('Please specify valid transaction'));
291 |
292 | if ($isTransaction) {
293 | return $transaction->transaction_id;
294 | }
295 |
296 | return $transaction;
297 | }
298 |
299 | /**
300 | * Define success and fail routes
301 | *
302 | * @param string $controller
303 | * @param string $successMethod
304 | * @param string $failMethod
305 | *
306 | * @return void
307 | */
308 | public static function routes(string $controller = \App\Http\Controllers\PayzeController::class, string $successMethod = 'success', string $failMethod = 'fail'): void
309 | {
310 | Route::prefix('payze')->name('payze.')->group(function () use ($controller, $successMethod, $failMethod) {
311 | Route::get('success', [$controller, $successMethod])->name('success');
312 | Route::get('fail', [$controller, $failMethod])->name('fail');
313 | });
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Payze.io Integration Package
2 |
3 | [](https://packagist.org/packages/payzeio/laravel-payze)
4 | [](https://packagist.org/packages/payzeio/laravel-payze)
5 | [](https://packagist.org/packages/payzeio/laravel-payze)
6 |
7 | This package allows you to process payments with Payze.io from your Laravel application.
8 |
9 | 
10 |
11 | ### Changelog
12 |
13 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
14 |
15 | ### Upgrading
16 |
17 | After upgrading to a newer version, make sure to run [publish command](#publish-migrations-and-config-by-running) to publish the latest migrations. Also, please copy new [config file](config/payze.php) contents to your existing one.
18 |
19 | ## Table of Contents
20 |
21 | - [Installation](#installation)
22 | - [API Keys](#api-keys)
23 | - [Define Routes](#define-routes)
24 | - [Config](#config)
25 | - [Log](#log)
26 | - [SSL Verification](#ssl-verification)
27 | - [Routes](#routes)
28 | - [Views](#views)
29 | - [Transactions Table](#transactions-table)
30 | - [Logs Table](#logs-table)
31 | - [Card Tokens Table](#card-tokens-table)
32 | - [API Key](#api-key)
33 | - [API Secret](#api-secret)
34 | - [Payments & Requests](#payments--requests)
35 | - [Just Pay](#just-pay)
36 | - [Add (Save) Card](#add-save-card)
37 | - [Pay with a Saved Card](#pay-with-a-saved-card)
38 | - [Commit](#commit)
39 | - [Refund](#refund)
40 | - [Transaction Info](#transaction-info)
41 | - [Merchant's Balance](#merchants-balance)
42 | - [Payment Request Options](#payment-request-options)
43 | - [Amount](#amount)
44 | - [Currency](#currency)
45 | - [Language](#language)
46 | - [Preauthorize](#preauthorize)
47 | - [Associated Model](#associated-model)
48 | - [Split Money](#split-money)
49 | - [Raw Data](#raw-data)
50 | - [Controller](#controller)
51 | - [Events](#events)
52 | - [Relationships](#relationships)
53 | - [Transactions Relationship](#transactions-relationship)
54 | - [Cards Relationship](#cards-relationship)
55 | - [Models](#models)
56 | - [Transaction Model](#transaction-model)
57 | - [Card Token Model](#card-token-model)
58 | - [Log Model](#log-model)
59 | - [Abandoned Transactions](#abandoned-transactions)
60 | - [Authors](#authors)
61 |
62 | ## Installation
63 |
64 | ```
65 | composer require payzeio/laravel-payze
66 | ```
67 |
68 | #### For Laravel <= 5.4
69 |
70 | If you're using Laravel 5.4 or lower, you have to manually add a service provider in your `config/app.php` file.
71 | Open `config/app.php` and add `PayzeServiceProvider` to the `providers` array.
72 |
73 | ```php
74 | 'providers' => [
75 | # Other providers
76 | PayzeIO\LaravelPayze\PayzeServiceProvider::class,
77 | ],
78 | ```
79 |
80 | #### Publish migrations and config by running:
81 |
82 | ```
83 | php artisan vendor:publish --provider="PayzeIO\LaravelPayze\PayzeServiceProvider"
84 | ```
85 |
86 | And run migrations:
87 |
88 | ```
89 | php artisan migrate
90 | ```
91 |
92 | ### API Keys
93 |
94 | Go to [Payze.io](https://payze.io) website and generate an API key. Place key and secret to .env file:
95 |
96 | ```
97 | PAYZE_API_KEY=PayzeApiKey
98 | PAYZE_API_SECRET=PayzeApiSecret
99 | ```
100 |
101 | ### Define Routes
102 |
103 | You have to define success and fail routes in your application in order to finalize transactions. Go to your `web.php` (or wherever you store routes) and add the following:
104 |
105 | `routes()` function takes 3 **optional** parameters:
106 |
107 | **Controller:** Controller name, default: `App\Http\Controller\PayzeController`
108 |
109 | **Success Method:** Success method name, default: `success`
110 |
111 | **Fail Method:** Fail method name, default: `fail`
112 |
113 | ```php
114 | use PayzeIO\LaravelPayze\Facades\Payze;
115 |
116 | // Other routes...
117 |
118 | Payze::routes();
119 | ```
120 |
121 | These routes will have the names `payze.success` and `payze.fail`. If you have defined them under some namespace, then you can update names in config. For example, if you defined payze routes in api.php and that file has the name `api.`, then your routes will be `api.payze.success` and `api.payze.fail`. Update them in `config/payze.php` file, stored in `routes` array.
122 |
123 | ## Config
124 |
125 | The general variables are stored in `config/payze.php` file. More details:
126 |
127 | ### Log
128 |
129 | Enable/disable database detailed logging on every request/transaction. By default, the log is enabled on the local environment only. You can override the value from `.env` or directly from the config file.
130 |
131 | ### SSL Verification
132 |
133 | Enable/Disable SSL verification in Guzzle client to avoid SSL problems on some servers. Enabled by default.
134 |
135 | ### Routes
136 |
137 | Success and fail routes names, which are used to identify the finished transactions and update transaction status in the database.
138 |
139 | Update route names only if you have defined routes in a different namespace (like `api`). For example, you will have `api.payze.success` and `api.payze.fail` URLs.
140 |
141 | ### Views
142 |
143 | Success and fail view names, which are displayed after a transaction is complete. You can override them and use your own pages with your own layout.
144 |
145 | By default, it uses an empty page with just status text (green/red colors) and a "return home" button.
146 |
147 | ### Transactions Table
148 |
149 | The name of the table in the database, which is used to store all the transactions.
150 |
151 | ### Logs Table
152 |
153 | The name of the table in the database, which is used to store detailed logs about transactions and API requests.
154 |
155 | ### Card Tokens Table
156 |
157 | The name of the table in the database, which is used to store all the saved card tokens.
158 |
159 | ### API Key
160 |
161 | API key of your [Payze.io](https://payze.io) account.
162 |
163 | ### API Secret
164 |
165 | API secret of your [Payze.io](https://payze.io) account.
166 |
167 | ## Payments & Requests
168 |
169 | All the requests are sent by corresponding classes, which extends the same class (PayzeIO\LaravelPayze\Concerns\ApiRequest).
170 |
171 | All requests are called statically by `request()` function (passing constructor data), then chain all the needed data and then `process()`.
172 |
173 | Detailed instructions about needed data and options are in the [next section](#payment-request-options).
174 |
175 | ### Just Pay
176 |
177 | If you need a one-time payment, then you should use the Just Pay function.
178 |
179 | **Parameters:**
180 |
181 | - `Amount` - `float`, required
182 |
183 | **Return:** `Illuminate\Http\RedirectResponse`
184 |
185 | ```php
186 | use PayzeIO\LaravelPayze\Requests\JustPay;
187 |
188 | return JustPay::request(1)
189 | ->for($order) // optional
190 | ->preauthorize() // optional
191 | ->process();
192 | ```
193 |
194 | ### Add (Save) Card
195 |
196 | Saving a card gives you a card token which you use for further manual charges without customer interaction. You can charge any amount and also save a card in one action, or you can set the amount to 0 to just save a card (Some banks may charge 0.1GEL and refund for saving a card).
197 |
198 | Card tokens are saved in [database](#card-tokens-table) and can be accessed by [PayzeCardToken](src/Models/PayzeCardToken.php) model or [cards relationship](#cards-relationship).
199 |
200 | After requesting a payment, a card token is created in a database with an inactive status. After a successful charge, the card token becomes active automatically.
201 |
202 | **IMPORTANT!** If you want to associate a card token with the user and a transaction with an order, then you should use `assignTo` method, which receives a model instance of the owner of a card token.
203 |
204 | **Parameters:**
205 |
206 | - `Amount` - `float`, optional, default: `0`
207 |
208 | **Methods:**
209 |
210 | - `assignTo` - `Illuminate\Database\Eloquent\Model`, optional, default: `null`
211 |
212 | **Return:** `Illuminate\Http\RedirectResponse`
213 |
214 | ```php
215 | use PayzeIO\LaravelPayze\Requests\AddCard;
216 |
217 | return AddCard::request(1)
218 | ->for($order) // transaction will be assigned to order
219 | ->assignTo($user) // optional: card token will be assigned to user. if not present, then will be assigned to order
220 | ->process();
221 | ```
222 |
223 | ### Pay with a Saved Card
224 |
225 | You can pay with a saved card token anytime without customer interaction.
226 |
227 | Card tokens can be accessed by [PayzeCardToken](src/Models/PayzeCardToken.php) model or [cards relationship](#cards-relationship). Read more about [card tokens model here.](#card-token-model)
228 |
229 | **Parameters:**
230 |
231 | - `CardToken` - `PayzeIO\LaravelPayze\Models\PayzeCardToken`, required
232 | - `Amount` - `float`, optional, default: `0`
233 |
234 | **Return:** `PayzeIO\LaravelPayze\Models\PayzeTransaction`
235 |
236 | ```php
237 | use PayzeIO\LaravelPayze\Requests\PayWithCard;
238 |
239 | // Get user's non-expired, default card
240 | $card = $user->cards()->active()->default()->firstOrFail();
241 |
242 | return PayWithCard::request($card, 15)
243 | ->for($order) // optional
244 | ->process();
245 | ```
246 |
247 | ### Commit
248 |
249 | Commit (charge) a blocked ([preauthorized](#preauthorize)) transaction.
250 |
251 | **Parameters:**
252 |
253 | - `TransactionId` - `string|PayzeIO\LaravelPayze\Models\PayzeTransaction`, required
254 | - `Amount` - `float`, optional, default: `0`, (can be partially charged). 0 will charge full amount
255 |
256 | **Return:** `PayzeIO\LaravelPayze\Models\PayzeTransaction`
257 |
258 | ```php
259 | use PayzeIO\LaravelPayze\Requests\Commit;
260 |
261 | return Commit::request($transaction)->process();
262 | ```
263 |
264 | ### Refund
265 |
266 | Refund a [refundable](#refundable-scope) transaction.
267 |
268 | **Parameters:**
269 |
270 | - `TransactionId` - `string|PayzeIO\LaravelPayze\Models\PayzeTransaction`, required
271 | - `Amount` - `float`, optional, default: `0`, (can be partially refunded). 0 will refund full amount
272 |
273 | **Return:** `PayzeIO\LaravelPayze\Models\PayzeTransaction`
274 |
275 | ```php
276 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
277 | use PayzeIO\LaravelPayze\Requests\Refund;
278 |
279 | $transaction = PayzeTransaction::refundable()->latest()->firstOrFail();
280 |
281 | return Refund::request($transaction)->process();
282 | ```
283 |
284 | ### Transaction Info
285 |
286 | Get transaction info and update in the database.
287 |
288 | **Parameters:**
289 |
290 | - `TransactionId` - `string|PayzeIO\LaravelPayze\Models\PayzeTransaction`, required
291 |
292 | **Return:** `PayzeIO\LaravelPayze\Models\PayzeTransaction`
293 |
294 | ```php
295 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
296 | use PayzeIO\LaravelPayze\Requests\GetTransactionInfo;
297 |
298 | $transaction = PayzeTransaction::latest()->firstOrFail();
299 |
300 | return GetTransactionInfo::request($transaction)->process();
301 | ```
302 |
303 | ### Merchant's balance
304 |
305 | Get balance info from the merchant's account.
306 |
307 | **Return:** `array`
308 |
309 | ```php
310 | use PayzeIO\LaravelPayze\Requests\GetBalance;
311 |
312 | return GetBalance::request()->process();
313 | ```
314 |
315 | ## Payment Request Options
316 |
317 | You can pass these parameters to all the payment requests in Payze package.
318 |
319 | ### Amount
320 |
321 | All payment requests have an amount in the constructor, but also there is a separate method for changing the amount.
322 |
323 | ```php
324 | use PayzeIO\LaravelPayze\Requests\JustPay;
325 |
326 | // Request 1 GEL originally
327 | $request = JustPay::request(1);
328 |
329 | // Some things happened, updating amount
330 | return $request->amount(10)->process();
331 | ```
332 |
333 | ### Currency
334 |
335 | You can change your payment's currency by calling `currency()` function on the request. Default: `GEL`
336 |
337 | See supported currencies in [currencies enum file](src/Enums/Currency.php).
338 |
339 | **Recommended:** Pass currency by using an enum instead of directly passing a string.
340 |
341 | ```php
342 | use PayzeIO\LaravelPayze\Enums\Currency;
343 | use PayzeIO\LaravelPayze\Requests\JustPay;
344 |
345 | return JustPay::request(1)->currency(Currency::USD)->process();
346 | ```
347 |
348 | ### Language
349 |
350 | You can change your payment page's language by calling `language()` function on the request. Default: `ge`
351 |
352 | See supported languages in [languages enum file](src/Enums/Language.php).
353 |
354 | **Recommended:** Pass language by using an enum instead of directly passing a string.
355 |
356 | ```php
357 | use PayzeIO\LaravelPayze\Enums\Language;
358 | use PayzeIO\LaravelPayze\Requests\JustPay;
359 |
360 | return JustPay::request(1)->language(Language::ENG)->process();
361 | ```
362 |
363 | ### Preauthorize
364 |
365 | Preauthorize method is used to block the amount for some time and then manually charge ([commit](#commit)) the transaction. For example, if you are selling products which have to be produced after the order, block (preauthorize) transaction on order and manually charge ([commit](#commit)) after your product are ready.
366 |
367 | ### Associated Model
368 |
369 | You can associate any Eloquent model to a transaction by calling `for()` function on the request. For example, pass an order instance to a payment request for checking the order's payment status after payment.
370 |
371 | ```php
372 | use App\Models\Order;
373 | use PayzeIO\LaravelPayze\Requests\JustPay;
374 |
375 | $order = Order::findOrFail($orderId);
376 |
377 | return JustPay::request(1)->for($order)->process();
378 | ```
379 |
380 | ### Split Money
381 |
382 | You can split the money into different bank accounts. For example, you have a marketplace where users sell their products, and you get a commission for that. You can simply split transferred money easily instead of manually transferring from a bank account to a seller on every order.
383 |
384 | You have to call `split()` function on the request, which accepts list/array of `PayzeIO\LaravelPayze\Objects\Split` object(s).
385 |
386 | Split object has three parameters: `Amount`, `Receiver's IBAN`, and `Pay In (optional)` (delay in days before transferring the money).
387 |
388 | For example, the cost of a product is 20GEL. You have to get your commission (10%) and transfer the rest to a seller.
389 |
390 | ```php
391 | use PayzeIO\LaravelPayze\Objects\Split;
392 | use PayzeIO\LaravelPayze\Requests\JustPay;
393 |
394 | return JustPay::request(20)
395 | ->split(
396 | new Split(2, "Your IBAN"), // Transfer 2GEL immediately
397 | new Split(18, "Seller's IBAN", 3) // Transfer 18GEL after 3 days (for example, as an insurance before processing the order)
398 | )->process();
399 | ```
400 |
401 | ### Raw Data
402 |
403 | By default, when you request a payment, it will return a RedirectResponse. You can call `raw()` function and payment request will return the original data instead of a RedirectResponse.
404 |
405 | ```php
406 | use PayzeIO\LaravelPayze\Requests\JustPay;
407 |
408 | $request = JustPay::request(20)->raw()->process();
409 |
410 | log($request['transactionId']);
411 |
412 | return $request['transactionUrl'];
413 | ```
414 |
415 | ## Controller
416 |
417 | Default controller should be published after running publish command from [Installation section](#installation). You can add your custom logic to your controller in `successResponse` and `failResponse` methods. For example, you can set flash message, send notifications, mark order as complete or anything you want from that methods.
418 |
419 | **IMPORTANT!** If you return any non-NULL value from `successResponse` and `failResponse` methods, then that response will be used on success/fail routes. Otherwise [default logic](#views) will be used.
420 |
421 | ## Events
422 |
423 | Events are fired after successful or failed transactions. You can [define listeners](https://laravel.com/docs/8.x/events#defining-listeners) in your application in order to mark an order as paid, notify a customer or whatever you need.
424 |
425 | Both events have `$transaction` property.
426 |
427 | Paid Event: `PayzeIO\LaravelPayze\Events\PayzeTransactionPaid`
428 |
429 | Failed Event: `PayzeIO\LaravelPayze\Events\PayzeTransactionFailed`
430 |
431 | ## Relationships
432 |
433 | You can add `transactions` and `cards` relationships to your models with traits to easily access associated entries.
434 |
435 | ### Transactions Relationship
436 |
437 | Add `HasTransactions` trait to your model.
438 |
439 | ```php
440 | use PayzeIO\LaravelPayze\Traits\HasTransactions;
441 |
442 | class Order extends Model
443 | {
444 | use HasTransactions;
445 | }
446 | ```
447 |
448 | Now you can access transactions by calling `$order->transactions`.
449 |
450 | ### Cards Relationship
451 |
452 | Add `HasCards` trait to your model.
453 |
454 | ```php
455 | use PayzeIO\LaravelPayze\Traits\HasCards;
456 |
457 | class User extends Model
458 | {
459 | use HasCards;
460 | }
461 | ```
462 |
463 | Now you can access saved cards by calling `$user->cards`.
464 |
465 | ## Models
466 |
467 | ### Transaction Model
468 |
469 | You can access all transactions logged in the database by `PayzeIO\LaravelPayze\Models\PayzeTransaction` model.
470 |
471 | Get all transactions:
472 |
473 | ```php
474 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
475 |
476 | PayzeTransaction::all();
477 | ```
478 |
479 | #### Paid Scope
480 |
481 | Filter paid transactions with `paid()` scope.
482 |
483 | ```php
484 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
485 |
486 | PayzeTransaction::paid()->get();
487 | ```
488 |
489 | #### Unpaid Scope
490 |
491 | Filter unpaid transactions with `unpaid()` scope.
492 |
493 | ```php
494 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
495 |
496 | PayzeTransaction::unpaid()->get();
497 | ```
498 |
499 | #### Completed Scope
500 |
501 | Filter completed transactions with `completed()` scope.
502 |
503 | ```php
504 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
505 |
506 | PayzeTransaction::completed()->get();
507 | ```
508 |
509 | #### Incomplete Scope
510 |
511 | Filter incomplete transactions with `incomplete()` scope.
512 |
513 | ```php
514 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
515 |
516 | PayzeTransaction::incomplete()->get();
517 | ```
518 |
519 | #### Refundable Scope
520 |
521 | Filter refundable transactions with `refundable()` scope.
522 |
523 | ```php
524 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
525 |
526 | PayzeTransaction::refundable()->get();
527 | ```
528 |
529 | #### Non-Refundable Scope
530 |
531 | Filter non-refundable transactions with `nonrefundable()` scope.
532 |
533 | ```php
534 | use PayzeIO\LaravelPayze\Models\PayzeTransaction;
535 |
536 | PayzeTransaction::nonrefundable()->get();
537 | ```
538 |
539 | ### Card Token Model
540 |
541 | You can access all saved card tokens logged in the database by `PayzeIO\LaravelPayze\Models\PayzeCardToken` model.
542 |
543 | **NOTICE:** After starting AddCard payment, new database entry is created with non-active token which gets activated after successful payment. So `Active` card refers to a valid token, which can be used in future payments.
544 |
545 | Get all active tokens:
546 |
547 | Tokens are automatically filtered by a global scope and only returns active tokens.
548 |
549 | ```php
550 | use PayzeIO\LaravelPayze\Models\PayzeCardToken;
551 |
552 | PayzeCardToken::all();
553 | ```
554 |
555 | Active card tokens have `card_mask`, `cardholder`, `brand`, `expiration_date` attributes, which can be helpful for a user to choose correct card.
556 |
557 | #### Active (Non-Expired) Scope
558 |
559 | Since tokens are automatically filtered by a global scope, `active()` scope now returns non-expired card tokens based on expiration date.
560 |
561 | Filter active (non-expired) card tokens with `active()` scope.
562 |
563 | ```php
564 | use PayzeIO\LaravelPayze\Models\PayzeCardToken;
565 |
566 | PayzeCardToken::active()->get();
567 | ```
568 |
569 | #### WithInactive Scope
570 |
571 | Tokens are automatically filtered by a global scope and only returns active tokens. If you want to include inactive card tokens in the list, you should add `withInactive()` scope to a query:
572 |
573 | ```php
574 | use PayzeIO\LaravelPayze\Models\PayzeCardToken;
575 |
576 | PayzeCardToken::withInactive()->get();
577 | ```
578 |
579 | #### Inactive Scope
580 |
581 | Filter inactive card tokens with `inactive()` scope. This method already includes `withInactive()` scope, so you don't have to specify it manually.
582 |
583 | ```php
584 | use PayzeIO\LaravelPayze\Models\PayzeCardToken;
585 |
586 | PayzeCardToken::inactive()->get();
587 | ```
588 |
589 | #### Is Expired
590 |
591 | You can check if already fetched PayzeCardToken model instance is expired or not.
592 |
593 | Method will return false if expiration date is not filled in database.
594 |
595 | ```php
596 | use PayzeIO\LaravelPayze\Models\PayzeCardToken;
597 |
598 | $token = PayzeCardToken::latest()->get();
599 |
600 | $token->isExpired();
601 | ```
602 |
603 | #### Is Active (Non-expired)
604 |
605 | You can check if already fetched PayzeCardToken model instance is expired or not.
606 |
607 | Method will return true if expiration date is not filled in database.
608 |
609 | ```php
610 | use PayzeIO\LaravelPayze\Models\PayzeCardToken;
611 |
612 | $card = PayzeCardToken::latest()->get();
613 |
614 | $card->isActive();
615 | ```
616 |
617 | #### Mark as Default
618 |
619 | You can set current card as a default. All other cards will be unmarked automatically.
620 |
621 | ```php
622 | use PayzeIO\LaravelPayze\Models\PayzeCardToken;
623 |
624 | $card = PayzeCardToken::latest()->get();
625 |
626 | $card->markAsDefault();
627 | ```
628 |
629 | ### Log Model
630 |
631 | You can access all logs from the database by `PayzeIO\LaravelPayze\Models\PayzeLog` model.
632 |
633 | Get all logs:
634 |
635 | ```php
636 | use PayzeIO\LaravelPayze\Models\PayzeLog;
637 |
638 | PayzeLog::all();
639 | ```
640 |
641 | ## Abandoned Transactions
642 |
643 | Abandoned transactions with status `Created` are automatically reject after about 10 minutes, so you have to run a scheduler to update those transactions' statuses.
644 |
645 | If you don't already have a scheduler configured, read [how to configure](https://laravel.com/docs/scheduling#running-the-scheduler) here.
646 |
647 | Register our console command in `app/Console/Kernel.php`'s `$commands` variable:
648 |
649 | ```php
650 | use PayzeIO\LaravelPayze\Console\Commands\UpdateIncompleteTransactions;
651 |
652 | protected $commands = [
653 | // Other commands
654 | UpdateIncompleteTransactions::class,
655 | ];
656 | ```
657 |
658 | Then add a command in a schedule in `app/Console/Kernel.php`'s `schedule` function. We recommend running a job every 30 minutes, but it's totally up to you and your application needs.
659 |
660 | ```php
661 | use PayzeIO\LaravelPayze\Console\Commands\UpdateIncompleteTransactions;
662 |
663 | protected function schedule(Schedule $schedule)
664 | {
665 | // Other commands
666 | $schedule->command(UpdateIncompleteTransactions::class)->everyThirtyMinutes();
667 | }
668 | ```
669 |
670 | ## Authors
671 |
672 | - [Levan Lotuashvili](https://github.com/Lotuashvili)
673 | - [All Contributors](../../contributors)
674 |
--------------------------------------------------------------------------------