Payment gateway added

pull/49/head
sadeghpm 3 months ago
parent 2bb6bf6931
commit 169196317d

@ -4,7 +4,7 @@ APP_KEY=
APP_DEBUG=true
APP_DEPLOYED=false
APP_TIMEZONE=UTC
APP_URL=http://localhost:8000
APP_URL=http://xshop.test
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
@ -20,12 +20,7 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
DB_CONNECTION=sqlite
SESSION_DRIVER=database
SESSION_LIFETIME=9999999
@ -45,7 +40,6 @@ MEMCACHED_HOST=127.0.0.1
PANEL_PREFIX=dashboard
PANEL_PAGE_COUNT=30
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
@ -60,7 +54,6 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
MEDIA_WATERMARK_SIZE=15
MEDIA_WATERMARK_OPACITY=50
@ -72,7 +65,6 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
XLANG_ACTIVE=false
XLANG_MAIN=en
XLANG_API_URL="http://5.255.98.77:3001"
@ -83,3 +75,6 @@ CURRENCY_CODE=USD
SIGN_SMS=true
SIGN_DRIVER=Kavenegar
ZARINPAL_MERCHANT=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PAY_GATEWAY=zarinpal

@ -0,0 +1,67 @@
<?php
namespace App\Contracts;
interface Payment
{
/**
* Register Payment Service Provider
*
* @return self
*/
public static function registerService();
/**
* Get Payment name
*
* @return string
*/
public static function getName(): string;
/**
* Get payment type must be one of: ONLINE, CHEQUE, CARD, CASH, CASH_ON_DELIVERY
*
* @return string
*/
public static function getType(): string;
/**
* Is Active To Show user
*
* @return bool
*/
public static function isActive(): bool;
/**
* Gateway Logo
*
* @return string
*/
public static function getLogo();
/**
* Request online payment
*
* @param int $amount transaction amount
* @param string $callbackUrl a url that callback user after transaction
* @param array $additionalData additional data to send back
*
* @return array request data like token and order id
* @throws \Throwable
*/
public function request(int $amount, string $callbackUrl, array $additionalData = []): array;
/**
* Redirect customer to bank payment page
*/
public function goToBank();
/**
* Verify payment
*
* @return array successful payment have two keys: reference_id , card_number
* @throws \Throwable if payment fail
*/
public function verify(): array;
}

@ -0,0 +1,40 @@
<?php
namespace App\Contracts;
interface PaymentStore
{
/**
* Store payment request
*
* @param int $orderId Payment unique order id
* @param null $token
* @param string $type One of 'ONLINE', 'CHEQUE', 'CASH', 'CARD', 'CASH_ON_DELIVERY'
*
* @return \App\Models\Payment
*/
public function storePaymentRequest($orderId,$amount, $token = null, $type = 'ONLINE',$bank=null): \App\Models\Payment;
/**
* Store success payment and update invoice status
*
* @param int $paymentId Payment unique order id
* @param string|int $referenceId Transaction reference id
* @param null $cardNumber
*
* @return \App\Models\Payment
*/
public function storeSuccessPayment($paymentId, $referenceId, $cardNumber = null): \App\Models\Payment;
/**
* Store failed payment and update invoice status
*
* @param int $orderId Payment unique order id
* @param null $message Fail reason text to store
*
* @return \App\Models\Payment
*/
public function storeFailPayment($orderId, $message = null): \App\Models\Payment;
}

@ -0,0 +1,21 @@
<?php
namespace App\Events;
use App\Models\Invoice;
use Illuminate\Queue\SerializesModels;
class InvoiceCompleted
{
use SerializesModels;
/**
* @var Invoice
*/
public $invoice;
public function __construct(Invoice $invoice)
{
$this->invoice = $invoice;
}
}

@ -0,0 +1,27 @@
<?php
namespace App\Events;
use App\Models\Invoice;
use App\Models\Payment;
use Illuminate\Queue\SerializesModels;
class InvoiceFailed
{
use SerializesModels;
/**
* @var Invoice
*/
public $invoice;
/**
* @var Payment
*/
public $payment;
public function __construct(Invoice $invoice,Payment $payment)
{
$this->invoice = $invoice;
$this->payment = $payment;
}
}

@ -0,0 +1,27 @@
<?php
namespace App\Events;
use App\Models\Invoice;
use App\Models\Payment;
use Illuminate\Queue\SerializesModels;
class InvoiceSucceed
{
use SerializesModels;
/**
* @var Invoice
*/
public $invoice;
/**
* @var Payment
*/
public $payment;
public function __construct(Invoice $invoice,Payment $payment)
{
$this->invoice = $invoice;
$this->payment = $payment;
}
}

@ -2,6 +2,8 @@
namespace App\Http\Controllers;
use App\Contracts\Payment;
use App\Models\Customer;
use App\Models\Discount;
use App\Models\Invoice;
use App\Models\Order;
@ -80,6 +82,7 @@ class CardController extends Controller
public function index()
{
auth('customer')->login(Customer::first());
$area = 'card';
$title = __("Shopping card");
$subtitle = '';
@ -97,27 +100,28 @@ class CardController extends Controller
]);
$total = 0;
// return $request->all();
$inv = new Invoice();
$inv->customer_id = auth('customer')->user()->id;
$inv->count = array_sum($request->count);
$inv->address_id = $request->address_id;
$inv->desc = $request->desc;
$invoice = new Invoice();
$invoice->customer_id = auth('customer')->user()->id;
$invoice->count = array_sum($request->count);
$invoice->address_id = $request->address_id;
$invoice->desc = $request->desc;
if ($request->has('transport_id')) {
$request->transport_id = $request->input('transport_id');
$t = Transport::find($request->input('transport_id'));
$inv->transport_price = $t->price;
$invoice->transport_price = $t->price;
$total += $t->price;
}
if ($request->has('discount_id')) {
$request->discount_id = $request->input('discount_id');
}
$inv->save();
$invoice->save();
foreach ($request->product_id as $i => $product) {
$order = new Order();
$order->product_id = $product;
$order->invoice_id = $inv->id;
$order->invoice_id = $invoice->id;
$order->count = $request->count[$i];
if ($request->quantity_id[$i] != '') {
$order->quantity_id = $request->quantity_id[$i];
@ -133,11 +137,39 @@ class CardController extends Controller
$order->save();
}
$inv->total_price = $total;
$inv->save();
$invoice->total_price = $total;
$invoice->save();
// clear shopping card
// self::clear();
return [$inv, $inv->orders];
//prepare to redirect to bank gateway
$activeGateway = config('xshop.payment.active_gateway');
/** @var Payment $gateway */
$gateway = app($activeGateway . '-gateway');
logger()->info('pay controller', ["active_gateway" => $activeGateway, "invoice" => $invoice->toArray(),]);
if ($invoice->isCompleted()) {
return redirect()->back()->with('message', __('Invoice payed.'));
}
$callbackUrl = route('pay.check', ['invoice_hash' => $invoice->hash, 'gateway' => $gateway->getName()]);
$payment = null;
try {
$response = $gateway->request(($invoice->total_price - $invoice->credit_price), $callbackUrl);
$payment = $invoice->storePaymentRequest($response['order_id'], ($invoice->total_price - $invoice->credit_price), $response['token'] ?? null, null, $gateway->getName());
session(["payment_id" => $payment->id]);
\Session::save();
return $gateway->goToBank();
} catch (\Throwable $exception) {
$invoice->status = 'FAILED';
$invoice->save();
\Log::error("Payment REQUEST exception: " . $exception->getMessage());
\Log::warning($exception->getTraceAsString());
$result = false;
$message = __('error in payment. contact admin.');
return redirect()->back()->withErrors($message);
}
}

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Payment;
use App\Contracts\Payment;
use App\Models\Invoice;
class GatewayVerifyController
{
/**
* @param Invoice $invoice
* @param Payment $gateway
*/
public function __invoke($invoice_hash, $gateway)
{
try {
$invoice = Invoice::whereHash($invoice_hash)->firstOrFail();
$payment = null;
$message = null;
$result = true;
$paymentId = self::getPayment($invoice);
$response = $gateway->verify();
$payment = $invoice->storeSuccessPayment($paymentId, $response['reference_id'], $response['card_number']);
session(['card'=>serialize([])]);
} catch (\Throwable $exception) {
$result = false;
$invoice->storeFailPayment($paymentId, $exception->getMessage());
$message = $exception->getMessage();
\Log::debug("Payment RESPONSE Fail For Gateway {$gateway->getName()} :" . $exception->getMessage() . " On Line {$exception->getLine()} Of File {$exception->getFile()}", ['request' => request()->all(), 'session' => request()->session()->all(), 'user' => request()->user(), 'payment_id' => $paymentId]);
\Log::warning($exception->getTraceAsString());
return redirect()->route('client.card')->withErrors(__("error in payment.").$message);
}
return redirect()->route('client.profile')->with('message' , __("payment success"));
}
/**
* @param Invoice $invoice
* @return integer
*/
public static function getPayment($invoice)
{
$paymentId = session('payment_id');
if (empty($paymentId)) {
$paymentId = $invoice->payments->last()->id;
}
return $paymentId;
}
}

@ -2,6 +2,8 @@
namespace App\Models;
use App\Events\InvoiceFailed;
use App\Events\InvoiceSucceed;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -9,6 +11,15 @@ class Invoice extends Model
{
use HasFactory;
const PENDING = 'PENDING';
const PROCESSING = 'PROCESSING';
const COMPLETED = 'COMPLETED';
const CANCELED = 'CANCELED';
const FAILED = 'FAILED';
protected $casts = [
'meta' => 'array',
];
public static $invoiceStatus = ['PENDING', 'CANCELED', 'FAILED', 'PAID', 'PROCESSING', 'COMPLETED'];
@ -16,6 +27,45 @@ class Invoice extends Model
{
return 'hash';
}
public function customer()
{
return $this->belongsTo(Customer::class);
}
public function payments()
{
return $this->hasMany(Payment::class);
}
public function successPayments()
{
return $this->hasMany(Payment::class)->where('status', 'COMPLETED');
}
public function payByBankUrl($gateway)
{
return route('redirect.bank', ['invoice' => $this->id, 'gateway' => $gateway]);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function products()
{
return $this->belongsToMany(Product::class, 'invoice_product')
->withPivot(
'count',
'price_total',
'data',
'quantity_id'
);
}
public function isCompleted()
{
return $this->status == 'COMPLETED' or $this->status == 'PROCESSING';
}
public function orders()
@ -30,4 +80,70 @@ class Invoice extends Model
$model->hash = generateUniqueID((strlen(Invoice::count()) + 2));
});
}
public function storePaymentRequest($orderId,$amount, $token = null, $type = 'ONLINE', $bank = null): \App\Models\Payment
{
$payment = new Payment();
$payment->order_id = $orderId;
$payment->type = $type?$type:'ONLINE';
$payment->amount=$amount;
$payment->meta = [
'fingerprint' => \Request::fingerprint(),
'bank' => $bank,
'token' => $token,
'ip' => \Request::ip(),
'auth_user' => \Auth::id(),
'user_agent' => \Request::userAgent(),
];
/** @var \App\Models\Invoice $this */
$this->payments()->save($payment);
// $payment->save();
return $payment;
}
public function storeSuccessPayment($paymentId, $referenceId, $cardNumber = null): \App\Models\Payment
{
/** @var Payment $payment */
$payment = Payment::findOrFail($paymentId);
$payment->reference_id = $referenceId;
$payment->meta = array_merge($payment->meta, ['card_number' => $cardNumber]);
$payment->status = "SUCCESS";
$payment->save();
/** @var \App\Models\Invoice $this */
$this->status = "COMPLETED";
$this->save();
try {
event(new InvoiceSucceed($this, $payment));
}catch (\Throwable $exception){
\Log::debug('Error In Event OrderSucceed. But Process Continued!',compact('payment'));
\Log::warning($exception->getMessage(),[$exception->getTraceAsString()]);
}
return $payment;
}
public function storeFailPayment($paymentId, $message = null): \App\Models\Payment
{
try {
/** @var Payment $payment */
$payment = Payment::findOrFail($paymentId);
if ($payment->status === Payment::SUCCESS) {
return $payment;
}
$payment->status = Payment::FAIL;
$payment->comment = $message;
$payment->save();
} catch (\Throwable $exception) {
$payment = new Payment();
}
$this->status = "FAILED";
/** @var \App\Models\Invoice $this */
$this->save();
event(new InvoiceFailed($this, $payment));
return $payment;
}
}

@ -8,6 +8,14 @@ use Illuminate\Database\Eloquent\Model;
class Payment extends Model
{
use HasFactory;
const PENDING = 'PENDING';
const SUCCESS = 'SUCCESS';
const FAIL = 'FAIL';
const CANCEL = 'CANCEL';
protected $casts = [
'meta' => 'array',
];
public static $types = ['ONLINE', 'CHEQUE', 'CASH', 'CARD', 'CASH_ON_DELIVERY'];
public static $status = ['PENDING','SUCCESS', 'FAIL','CANCEL'];

@ -0,0 +1,120 @@
<?php
namespace App\Payment;
use App\Contracts\Payment;
class Zarinpal implements Payment
{
public $token;
/**
* @var \Pishran\Zarinpal\RequestResponse
*/
public $result;
/**
* @var \Pishran\Zarinpal\Zarinpal
*/
private $gateway;
public function __construct(\Pishran\Zarinpal\Zarinpal $gateway)
{
$this->gateway = $gateway;
}
/**
* Get Payment name
*
* @return string
*/
public static function getName(): string
{
return 'zarinpal';
}
public static function getType(): string
{
return 'ONLINE';
}
/**
* Request online payment
*
* @param int $amount transaction amount
* @param string $callbackUrl return user after transaction to this url
* @param array $additionalData additional data to send back
*
* @return array request data like token,order_id
* @throws \Exception
*/
public function request(int $amount, string $callbackUrl, array $additionalData = []): array
{
$result = $this->gateway->amount($amount )->request()->callbackUrl($callbackUrl)->description(config('app.name'))->send();
throw_unless($result->success(), \Exception::class, $result->error()->message());
\Session::put('zarinpal_amount', $amount);
\Session::put('zarinpal_token', $result->authority());
\Session::save();
$this->token = $result->authority();
$this->result = $result;
return [
'order_id' => $result->authority(),
'token' => null
];
}
/**
* Redirect customer to bank payment page
*/
public function goToBank()
{
return redirect()->away($this->result->url());
}
/**
* Verify payment
* @return array successful payment result.The array contain 2 keys: card_number, reference_id. The reference_id is reference number in banking network
* @throws \Throwable if payment fail
*/
public function verify(): array
{
$result = $this->gateway->amount(session('zarinpal_amount'))
->verification()
->authority(session('zarinpal_token'))
->send();
throw_if(
!$result->success(),
\Exception::class,
$result->error()->message()
);
return [
'reference_id' => $result->referenceId(),
'card_number' => $result->cardPan(),
];
}
public static function registerService()
{
app()->singleton(
sprintf('%s-gateway', self::getName()),
function () {
$gateway = zarinpal()
->merchantId(config('xshop.payment.config.zarinpal.merchant'));
return new Zarinpal($gateway);
}
);
}
public static function isActive(): bool
{
return !empty(config('xshop.payment.config.zarinpal.merchant'));
}
public static function getLogo()
{
return asset('payment/image/shaparak.png');
}
}

@ -0,0 +1,101 @@
<?php
namespace App\Payment;
use App\Contracts\Payment;
class Zibal implements Payment
{
/**
* @var \Dpsoft\Zibal\Zibal
*/
private $gateway;
public function __construct(\Dpsoft\Zibal\Zibal $gateway)
{
$this->gateway = $gateway;
}
/**
* Get Payment name
*
* @return string
*/
public static function getName(): string
{
return 'zibal';
}
public static function getType(): string
{
return 'ONLINE';
}
/**
* Request online payment
*
* @param int $amount transaction amount
* @param string $callbackUrl a url that callback user after transaction
* @param array $additionalData additional data to send back
* @return array request data like token,order_id
* @throws \Exception
*/
public function request(int $amount, string $callbackUrl, array $additionalData = []): array
{
$result = $this->gateway->request($callbackUrl, $amount);
\Session::put('zibal_amount', $amount);
\Session::put('zibal_invoice_id', $result['invoice_id']);
\Session::put('zibal_token', $result['token']);
return [
'order_id' => $result['invoice_id'],
'token' => $result['token']
];
}
/**
* Redirect customer to bank payment page
*/
public function goToBank()
{
return redirect()->away($this->gateway->redirectUrl());
}
/**
* Verify payment
* @return array successful payment result.The array contain 3 key: card_number, invoice_id & reference_id. The reference_id is reference number in banking network
* @throws \Exception if payment fail
*/
public function verify(): array
{
$result = $this->gateway->verify(session('zibal_amount'), session('zibal_token'));
return [
'reference_id' => $result['transaction_id'],
'card_number' => $result['card_number'],
];
}
public static function registerService()
{
app()->singleton(
sprintf('%s-gateway',self::getName()),
function () {
$gateway = new \Dpsoft\Zibal\Zibal(config('xshop.payment.config.zibal.merchant'));
return new Zibal($gateway);
}
);
}
public static function isActive():bool
{
return !empty(config('xshop.payment.config.zibal.merchant'));
}
public static function getLogo()
{
return asset('payment/image/shaparak.png');
}
}

@ -24,6 +24,14 @@ class AppServiceProvider extends ServiceProvider
$this->commands([
TranslatorCommand::class,
]);
foreach (config('xshop.payment.gateways') as $gateway){
/** @var \App\Contracts\Payment $gateway */
$gateway::registerService();
}
\Route::bind('gateway', function ($gatewayName) {
return app("$gatewayName-gateway");
});
}
/**

@ -0,0 +1,19 @@
<?php
return [
"payment" => [
'active_gateway' => env('PAY_GATEWAY', \App\Payment\Zarinpal::getName()),
'gateways' => [
\App\Payment\Zibal::class,
\App\Payment\Zarinpal::class,
],
'config' => [
'zibal' => [
'merchant' => env('ZIBAL_MERCHANT', 'zibal'),
],
'zarinpal' => [
'merchant' => env('ZARINPAL_MERCHANT'),
'test' => env('ZARINPAL_TEST')
],
],
]
];

@ -436,6 +436,9 @@ Route::get('whoami', function () {
return \Auth::guard('customer')->user();
})->name('whoami');
//Route::get('/payment/redirect/bank/{invoice}/{gateway}', \App\Http\Controllers\Payment\GatewayRedirectController::class)->name('redirect.bank');
Route::any('/payment/check/{invoice_hash}/{gateway}', \App\Http\Controllers\Payment\GatewayVerifyController::class)->name('pay.check');
Route::any('{lang}/{any}', [ClientController::class, 'lang'])
->where('any', '.*')
->where('lang','[A-Za-z]{2}')

Loading…
Cancel
Save