Skip to content

Instantly share code, notes, and snippets.

@kauhat
Last active August 14, 2017 14:21
Show Gist options
  • Save kauhat/1d29d531ffdd2a3be0de3d70ed4346a3 to your computer and use it in GitHub Desktop.
Save kauhat/1d29d531ffdd2a3be0de3d70ed4346a3 to your computer and use it in GitHub Desktop.
Laravel Omnipay - Sagepay Example

This has been cobbled together from an example of the library with some additional routes and controllers for Laravel.

There's a few dodgy things happenning here; failed payments aren't properly handled and transaction data is stored in the cache. This should really be in it's own table.

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Cache;
use Omnipay;
use Omnipay\Common\CreditCard;
use Omnipay\Common\Exception\InvalidResponseException;
use Omnipay\SagePay\Message\ServerCompleteAuthorizeResponse;
class PaymentController extends Controller
{
private function getTransactionId(Order $order) {
// Make some shit up here.
return $transactionId;
}
// This happens when the user submits the order.
public function checkout(Request $request)
{
// Get the transaction ID for the order
$transactionId = $this->getTransactionId($order);
// Actual credit card data entered on remote payment page.
$card = new CreditCard(
[
'firstName' => $order->name_first,
'lastName' => $order->name_last,
'billingAddress1' => $order->address->street_1,
'billingAddress2' => $order->address->street_2,
'billingCity' => $order->address->city,
'billingState' => $order->address->county,
'billingPostcode' => $order->address->postcode,
'billingCountry' => $order->address->country,
'shippingAddress1' => $order->address->street_1,
'shippingAddress2' => $order->address->street_2,
'shippingCity' => $order->address->city,
'shippingState' => $order->address->county,
'shippingPostcode' => $order->address->postcode,
'shippingCountry' => $order->address->country,
]
);
// May be able to add line items here.
// https://github.com/thephpleague/omnipay/pull/154
// Create transaction with remote API.
$response = Omnipay::purchase(
[
'amount' => number_format($amount, 2, '.', ''),
'transactionId' => $transactionId,
'card' => $card,
'currency' => 'GBP',
'description' => 'Description',
'notifyUrl' => route('api.payment.process', [$order]),
]
)->send();
// Store the transaction status in the cache.
// Probably not a good idea, should really go into the database proper
$transaction = Cache::forever(
$transactionId, [
'finalStatus' => 'PENDING',
'status' => method_exists($response, 'getStatus') ? $response->getStatus() : $response->getCode(),
'message' => 'Awaiting notify',
'transactionReference' => $response->getTransactionReference(),
]
);
if ($response->isRedirect()) {
// Redirect the user to a payment page.
$response->redirect();
} else {
// SOMETHING FUCKED UP.
}
}
// Sagepay will post to this upon payment successful/failed.
public function process(Request $request)
{
// Get the transaction ID for the order.
$transactionId = $this->getTransactionId($order);
// Retrieve the transaction from the database.
$transaction = Cache::get($transactionId);
// I've forgotten what this does.
if (empty($transaction) || $transaction['finalStatus'] != 'PENDING') {
// vendorTxCode missing or invalid - aborting
$transactionReference = null;
} else {
$transactionReference = $transaction['transactionReference'];
}
// Get the "complete purchase" message.
$requestMessage = OmniPay::completePurchase(
[
'amount' => '0.01',
'transactionId' => $transactionId,
'transactionReference' => $transactionReference,
]
);
// Do a "send" - this will validate everything.
// This is mental.
try {
$responseMessage = $requestMessage->send();
} catch (\Exception $e) {
// InvalidResponseException will not catch a null transactionReference.
// You may want to catch them separately and return different error messages.
// This is a nasty hack, manually creating a message in the
// event of an exception caused by a security failure.
$requestMessage = OmniPay::completePurchase([]);
$responseMessage = new ServerCompleteAuthorizeResponse($requestMessage, []);
$responseMessage->invalid(route('order.failed', [$order]), $e->getMessage());
}
// Handle the actions based on successful (or not) authorisation
// of the transaction.
if ($responseMessage->isSuccessful()) {
$finalStatus = 'APPROVED';
// Do some magic.
$order->completed();
} else {
$finalStatus = 'REJECTED';
// This is tragic.
$order->failed();
}
// Store the result.
Cache::forever(
$transactionId, [
'finalStatus' => $finalStatus,
'status' => $responseMessage->getStatus(),
'message' => $responseMessage->getMessage(),
'notifyData' => $responseMessage->getData(),
]
);
// Return to SagePay with route end user will be redirected to.
$responseMessage->confirm(route('order.thanks', [$order]));
}
}
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// This needs to be in here as we don't want to deal with CRSF etc.
Route::post('payment/{organisation}/process', 'PaymentController@processTransaction')->name('api.payment.processTransaction');
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| This file is where you may define all of the routes that are handled
| by your application. Just tell Laravel the URIs it should respond
| to using a Closure or controller method. Build something great!
|
*/
// After checkout, "process" route in API routes
Route::get('payment', 'PaymentController@startTransaction')->name('startTransaction');
// After payment successful/failed
Route::group(
[
'prefix' => 'order',
'as' => 'order.'
],
function () {
Route::get('thanks', 'OrderController@completed')->name('completed');
Route::get('failed', 'OrderController@failed')->name('failed');
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment