Does anyone have a working example of Omnipay and Sagepay Server or Sagepay Direct (with 3D Secure)? - opayo

I'm struggling to get either to work and Omnipay doesn't come with much documentation. I've successfully used it for other payment gateways but not with Sagepay. I'm trying to integrate it into CodeIgniter but can work from examples in other frameworks - I'm getting desperate!

Thanks to some great help on github (see comments in my original post for the thread link), I now have some workable code which I will share here in case it helps someone else in the future.
<?php
use Omnipay\Omnipay;
class PaymentGateway {
//live details
private $live_vendor = 'xxx';
//test details
private $test_vendor= 'xxx';
//payment settings
private $testMode = true;
private $api_vendor = '';
private $gateway = null;
public function __construct()
{
parent::__construct();
//setup api details for test or live
if ($this->testMode) :
$this->api_vendor = $this->test_vendor;
else :
$this->api_vendor = $this->live_vendor;
endif;
//initialise the payment gateway
$this->gateway = Omnipay::create('SagePay_Server');
$this->gateway->setVendor($this->api_vendor);
$this->gateway->setTestMode($this->testMode);
}
public function initiate()
{
//get order details
$orderNo = customFunctionToGetOrderNo(); //get the order number from your system however you store and retrieve it
$params = array(
'description'=> 'Online order',
'currency'=> 'GBP',
'transactionId'=> $orderNo,
'amount'=> customFunctionToGetOrderTotal($orderNo)
);
$customer = customFunctionToGetCustomerDetails($orderNo);
$params['returnUrl'] = '/payment-gateway-process/' . $orderNo . '/'; //this is the Sagepay NotificationURL
$params['card'] = array(
'firstName' => $customer['billing_firstname'],
'lastName' => $customer['billing_lastname'],
'email' => $customer['billing_email'],
'billingAddress1' => $customer['billing_address1'],
'billingAddress2' => $customer['billing_address2'],
'billingCity' => $customer['billing_town'],
'billingPostcode' => $customer['billing_postcode'],
'billingCountry' => $customer['billing_country'],
'billingPhone' => $customer['billing_telephone'],
'shippingAddress1' => $customer['delivery_address1'],
'shippingAddress2' => $customer['delivery_address2'],
'shippingCity' => $customer['delivery_town'],
'shippingPostcode' => $customer['delivery_postcode'],
'shippingCountry' => $customer['delivery_country']
);
try {
$response = $this->gateway->purchase($params)->send();
if ($response->isSuccessful()) :
//not using this part
elseif ($response->isRedirect()) :
$reference = $response->getTransactionReference();
customFunctionToSaveTransactionReference($orderNo, $reference);
$response->redirect();
else :
//do something with an error
echo $response->getMessage();
endif;
} catch (\Exception $e) {
//do something with this if an error has occurred
echo 'Sorry, there was an error processing your payment. Please try again later.';
}
}
public function processPayment($orderNo)
{
$params = array(
'description'=> 'Online order',
'currency'=> 'GBP',
'transactionId'=> $orderNo,
'amount'=> customFunctionToGetOrderTotal($orderNo)
);
$customer = customFunctionToGetCustomerDetails($orderNo);
$transactionReference = customFunctionToGetTransactionReference($orderNo);
try {
$response = $this->gateway->completePurchase(array(
'transactionId' => $orderNo,
'transactionReference' => $transactionReference,
))->send();
customFunctionToSaveStatus($orderNo, array('payment_status' => $response->getStatus()));
customFunctionToSaveMessage($orderNo, array('gateway_response' => $response->getMessage()));
//encrypt it to stop anyone being able to view other orders
$encodeOrderNo = customFunctionToEncodeOrderNo($orderNo);
$response->confirm('/payment-gateway-response/' . $encodeOrderNo);
} catch(InvalidResponseException $e) {
// Send "INVALID" response back to SagePay.
$request = $this->gateway->completePurchase(array());
$response = new \Omnipay\SagePay\Message\ServerCompleteAuthorizeResponse($request, array());
customFunctionToSaveStatus($orderNo, array('payment_status' => $response->getStatus()));
customFunctionToSaveMessage($orderNo, array('gateway_response' => $response->getMessage()));
redirect('/payment-error-response/');
}
}
public function paymentResponse($encodedOrderNo)
{
$orderNo = customFunctionToDecode($encodedOrderNo);
$sessionOrderNo = customFunctionToGetOrderNo();
if ($orderNo != $sessionOrderNo) :
//do something here as someone is trying to fake a successful order
endif;
$status = customFunctionToGetOrderStatus($orderNo);
switch(strtolower($status)) :
case 'ok' :
customFunctionToHandleSuccess($orderNo);
break;
case 'rejected' :
case 'notauthed' :
//do something to handle failed payments
break;
case 'error' :
//do something to handle errors
break;
default:
//do something if it ever reaches here
endswitch;
}
}

I gave a talk last night about this, and have put the working demo scripts on github here:
https://github.com/academe/OmniPay-SagePay-Demo
SagePay Direct is a one-off action - OmniPay sends the transaction details and gets an immediate response.
SagePay Server involves a redirect of the user to the SagePay website to authorise the transaction using their card details. This API uses a notify message, where SagePay will call your application directly with the authorisation results. This happens outside of the user's session, and so requires the transaction to be stored in the database so it can be shared between the two transactions.
All this is in the scripts linked above. authorize.php will do the authorisation. Edit that to use SagePay\Direct or SagePay\Server to see how it works. The notification handler for SagePay\Server is sagepay-confirm.php and that ultimately sends the user to final.php where the result can be read from the transaction stored in the database.
The scripts are all commented and should make sense, but feel free to ask more questions about them here or in the issue tracker of that github repository.
I've not tried SagePay\Direct with 3D-Secure though. The scripts may need some modification to support that, assuming that combination is a thing.

Related

How to update invoice using XERO API?

I am using xero api to integrate it with my web app to manage invoices, currently i want to update invoice through invoice id, i have an helper xero.php file to handle crud operations. I have a function get invoice by invoice id, i want to update the InvoiceNumber. What is the best way to update invoice?
update_invoice_function
public function update_invoice(){
$invoice_id = '******-***-****-****-************';
$updated_invoice = Xero::find_invoice_by_id($invoice_id);
$updated_invoice['response']->TotalDiscount = "1";
$updated_invoice['response']->Date = "2020-01-20";
$updated_invoice['response']->Status = "DRAFT";
$get_invoice_response = Xero::update_invoice_by_id($invoice_id,$updated_invoice['response']);
dd($get_invoice_response);
}
update_invoice_by_id function
public static function update_invoice_by_id($invoice_id,$updated_invoice){
self::instanciate();
try{
$update = self::$xero->loadByGUID('Accounting\\Invoice',$invoice_id);
dd($update);
$update->jsonSerialize($updated_invoice);
$invoice_response = self::$xero->save($update);
$response = [
'error' => false,
'status' => 200,
'message' => 'Invoice updated successfully',
'response' => $invoice_response->getElements()
];
}
catch (Exception $e){
$response = [
'error' => true,
'status' => $e->getCode(),
'message' => $e->getMessage()
];
}
return $response;
}
we have an example app that shows some sample calls to things like createInvoice.. However worth noting that there was recently a breaking change for the newer version of the API to support batch calls for invoice Create & Updates:
Older Way
$result = $apiInstance->updateInvoice($xeroTenantId, $guid, $invoice);
New Way
-> updateOrCreateInvoices is the newest way.. I recommend looking at your version of the package you are running as the function has changed.
https://github.com/XeroAPI/xero-php-oauth2-app/blob/4bf74e915df1b0fee66f954ffcbdc331e762a06a/example.php#L1222
However - in general, doing a POST on an existing invoice with the invoice ID and the New Number will enable you to update it.
{
"InvoiceID": "292532ba-xxxx-xxxx-xxxx-60e7c39c4360",
"InvoiceNumber": "INV-im-a-new-number"
}
Hope this un-blocks you!

Laravel 5.3 - Omnipay Paypal Express not returning success message

I'm new to Laravel. I've been struggling to implement Paypal Express Checkout in my website for a couple days in order to enable donations to a non-profit organization. Thanks to these explanations I've been able to install Omnipay, let the user input the amount (s)he wants to donate and go to Paypal.
But, when I try to end the transaction (Pay), I'm not redirected to my succes message. My sandbox account does not show any transactions either, so it seems the payment is not completed correctly. I'm guessing there's something wrong with my "getSuccessPayment" function, but I can't figure out what it is...
Here's my Controller so far :
<?php namespace App\Http\Controllers;
use Omnipay\Omnipay;
use Session;
use App\Http\Requests\PaymentRequest;
class PaymentController extends Controller {
public function postPayment(PaymentRequest $request)
{
$price = $request->get('price');
$items[] = array('name' => 'Don', 'quantity' => 1, 'price' => $price);
$params = array(
'cancelUrl'=>url('/donner'),
'returnUrl'=>url('/payment_success'),
'amount' => $price,
'currency' => 'EUR'
);
Session::put('params', $params);
Session::save();
$gateway = Omnipay::create('PayPal_Express');
$gateway->setUsername('my sandbox email');
$gateway->setPassword('my sandbox password');
$gateway->setSignature('my sandbox signature');
$gateway->setTestMode(true);
$response = $gateway->purchase($params)->setItems($items)->send();
if ($response->isSuccessful()) {
print_r($response);
} elseif ($response->isRedirect()) {
$response->redirect();
} else {
echo $response->getMessage();
}
}
public function getSuccessPayment()
{
$gateway = Omnipay::create('PayPal_Express');
$gateway->setUsername('my sandbox email');
$gateway->setPassword('my sandbox password');
$gateway->setSignature('my sandbox signature');
$gateway->setTestMode(true);
$params = Session::get('params');
$response = $gateway->completePurchase($params)->send();
$paypalResponse = $response->getData();
if(isset($paypalResponse['PAYMENTINFO_0_ACK']) && $paypalResponse['PAYMENTINFO_0_ACK'] === 'Success') {
return redirect('/payment_success');
} else {
//payment fails
return redirect('/payment_failure');
}
}
}
?>
And my Routes :
Route::post('donner',
['as' => 'payment', 'uses' => 'PaymentController#postPayment']);
Route::get('payment_success', 'PaymentController#getSuccessPayment');
Route::get('payment_failure', 'PaymentController#getSuccessPayment');
When creating your gateway parameters you are passing in /donner as the returnUrl, this is where your users are returned to after completing the PayPal express login and payment confirmation so Laravel would look Route::get('donner'... route which you don't have, changing this to 'returnUrl'=>url('/payment_success'), will bring your users back to your success route and allow you to file the completePurchase call.
Edit for further details based on edited question and comments:
Customers are returned to your returnUrl if the successfully complete the PayPal login and checkout screens, they go to the cancelUrl if for whatever reason they quit the process.
In your PaymentController#getSuccessPayment method paypal will send back a token and payerID in the query string (www.example.com/payment_success?token=EC-12345&PayerID=ABC123, omnipay-paypal will automatically pick up on this in the completePurchase call which is where you are effective confirming with PayPal that the customer completed the checkout correctly and that the transaction was successful.
To avoid confusion I would rename your current Route::get('payment_success', 'PaymentController#getSuccessPayment'); route to something like Route::get('complete_payment', 'PaymentController#getCompletePayment'); and create a new payment_success route that a user is sent to after you have confirmed the status of the payment with PayPal.

Show error message after a page refresh

I have a login form that I'm doing 2 part validations: The client-side and the server side.
Client-side: check if valid email, check if password is minimal 6 characters, etc. If something goes wrong on the client-side I can show the user the error messages on the same page with the login form, because it page never got reloaded because it will not send a post to the server if the client-side validation isn't true.
But when it is true, then I'm doing a server side validation -> check if email and password matches to effectively log in into the website. But if the login credentials aren't matching, I need to show a error message. This is the part where I'm stuck. Where and how do I get a message on the same page (login form) because page gets posted and refreshed so I'm losing data. Now when credentials aren't correct I'm redirecting the user back to the login page, but without any messages. But I'm trying to achieve that he'll see the message 'credentials aren't correct'. Can someone help me with this?
Login View
<?php
$loginEmail = array('placeholder' => "Email", 'name' => "loginEmail");
$loginPassword = array('placeholder' => "Wachtwoord", 'name' => "loginPassword");
$loginSubmit = array('name' => "loginSubmit", 'class' => "btn", 'value' => "Inloggen");
echo form_open('login/inloggen', array('class' => 'grid-100 formc'));
echo form_input($loginEmail, set_value('loginEmail'));
echo form_password($loginPassword);
echo form_submit($loginSubmit);
echo form_close();
?>
Login Controller
function index(){
$logged_in = $this->logged_in->is_logged_in();
if($logged_in){
$this->load->view('profile_view');
}
else{
$data['content'] = 'login_view';
$this->load->view('templates/template', $data);
}
}
function inloggen(){
if($this->input->post('loginSubmit')){
if($this->form_validation->run('login_validation_rules') == FALSE){
$this->index();
}
else{
$this->load->model('login_model');
$query = $this->login_model->validate();
if($query){
$data = array(
'username' => $this->input->post('loginEmail'),
'is_logged_in' => true
);
$this->session->set_userdata($data);
redirect('profile');
}
else{
$this->index();// If credentials aren't correct, redirect them to login page. But how I set a message here?
}
}
}
}
Login Model
function validate(){
$this->db->where('email', $this->input->post('loginEmail'));
$this->db->where('password', md5($this->input->post('loginPassword')));
$query = $this->db->get('tbl_users');
if($query->num_rows == 1){
return true;
}
else{
return false;
}
}
I will prefer do to in one controller, and it will be more clear. I make one simple example, just to tell you the way how I think is good.
function login(){
$data['error_message'] = "";
$data['username'] = $this->input->post('username');
$data['password'] = $this->input->post('password');
if($this->input->post('loginSubmit')){
// Make rules of validation that need to be required
if($this->form_validation->run('login_validation_rules')){ // If true it goes IN
$this->load->model('login_model');
$query = $this->login_model->validate();
if($query){
$data = array(
'username' => $this->input->post('loginEmail'),
'is_logged_in' => true
);
$this->session->set_userdata($data);
redirect('profile'); // Go IN profile
} else {
$data['error_message'] = "Something went wrong"; // This error you write on View
}
}
}
$this->load->view('login_view', $data) //Load view of login
}
I didn't test, and it will need to adapt in your data, but I just want to explain the way. It you have any problem, write an comment, and we will find the solution together.
I have solved this problem like so: Basically when the model sends a false. I make var with a message in it and then in my index I check if that var is there or not.
The only thing I want to know is, is it okay to set a message in my controller or should I have done that somewhere elsewhere??
function index(){
$logged_in = $this->logged_in->is_logged_in();
if($logged_in){
$this->load->view('profile_view');
}
else{
//Check here if my redirection submitted also a message
if(isset($this->ongeldig)){
$data['ongeldig'] = $this->ongeldig;
}
else{
$data['ongeldig'] = '';
}
$data['content'] = 'login_view';
$this->load->view('templates/template', $data);
}
}
function inloggen(){
if($this->input->post('loginSubmit')){
if($this->form_validation->run('login_validation_rules')){
$this->load->model('login_model');
$query = $this->login_model->validate();
if($query){
$data = array(
'username' => $this->input->post('loginEmail'),
'is_logged_in' => true
);
$this->session->set_userdata($data);
redirect('profile');
}
else{// set a error message, when the models function returns false.
$this->ongeldig = "Ongeldig wachtwoord/gebruikersnaam";
$this->index();
}
}
else{
$this->index();
}
}
}

omnipay paypal express not returning address

I am using the omnipay setup here: https://github.com/adrianmacneil/omnipay to process a paypal express checkout.
The process works fine in that the user is redirected to paypal -> they login and choose to pay -> they get returned to my site at which point I capture the payment.
The problem I've got is that I need to capture the address they have entered into paypal as their billing / shipping address.
To send the user across to paypal I have the following:
$gateway = GatewayFactory::create('PayPal_Express');
$gateway->setUsername('XX-USERNAME_XX');
$gateway->setPassword('XX_PASSWORDXX');
$gateway->setSignature('XX_SIG_XX');
$gateway->setTestMode(true);
$response = $gateway->purchase(
array(
'cancelUrl'=>'http://www.XXX.co.uk/',
'returnUrl'=>'http://www.XXX.co.uk/paypalexpress_confirm',
'amount' => $totalamount,
'currency' => 'GBP'
)
)->send();
$response->redirect();
When the user is returned I have the following:
$gateway = GatewayFactory::create('PayPal_Express');
$gateway->setUsername('XX-USERNAME_XX');
$gateway->setPassword('XX_PASSWORDXX');
$gateway->setSignature('XX_SIG_XX');
$gateway->setTestMode(true);
$response = $gateway->completePurchase(
array(
'cancelUrl'=>'http://www.XXX.co.uk/',
'returnUrl'=>'http://www.XXX.co.uk/paypalexpress_confirm',
'amount' => $totalamount,
'currency' => 'GBP'
)
)->send();
echo $responsemsg=$response->getMessage();
echo '<br><br><br>';
$data = $response->getData();
print_r($data);
Nothing in the response message or the raw data contains the customer address.
Has anyone got this working as i'm struggling and it's the final step to complete the transaction.
For those who are trying to get this work it's as Adrian said.
You first do the normal omnipay paypal payment and then afterwards:
get the token you were given
preform a second call to paypal using the call getexpresscheckoutdetails method
this returns all the info you need
API info here: https://cms.paypal.com/uk/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_GetExpressCheckoutDetails
The php script paypal provide to do it all for you:
https://cms.paypal.com/cms_content/ES/es_ES/files/developer/nvp_ECGetExpressCheckout_php.txt
omnipay\paypal\ProGateway.php add new function
public function fetchExpressCheckoutDetail(array $parameters = array())
{
return $this->createRequest('\Omnipay\PayPal\Message\FetchExpressCheckoutRequest', $parameters);
}
omnipay\paypal\src\Message add new file FetchExpressCheckoutRequest.php
namespace Omnipay\PayPal\Message;
class FetchExpressCheckoutRequest extends AbstractRequest
{
public function getData()
{
$data = $this->getBaseData('GetExpressCheckoutDetails');
$this->validate('transactionReference');
$data['TOKEN'] = $this->getTransactionReference();
$url = $this->getEndpoint()."?USER={$data['USER']}&PWD={$data['PWD']}&SIGNATURE={$data['SIGNATURE']}&METHOD=GetExpressCheckoutDetails&VERSION={$data['VERSION']}&TOKEN={$data['TOKEN']}";
parse_str (file_get_contents( $url ),$output);
$data = array_merge($data,$output);
return $data;
}
}
Usage:
$response = $gateway->completePurchase($params)->send();
$data = $response->getData();
$gateway->fetchExpressCheckoutDetail(array('transactionReference'=>$data['TOKEN']))->getData();
It will be not the best. But it works. :)
If it's not returned by the $response->getData() method, you might need to call PayPal's GetExpressCheckoutDetails API method to get the extra details about the transaction.
Omnipay doesn't support this out of the box, so you will probably need to copy and customize one of the existing requests to make a separate API call after you have confirmed payment.

Display message after logout via Silex SecurityServiceProvider

I am using the SecurityServiceProvider to secure my Silex application and would like to display a message after the user has logged out by navigating to the logout_path route.
The message should be stored in the sessions flash bag so that my template can automatically display it after.
I have tried adding an application middleware, but where not able to hook my code in. The before hook doesn't seem to work, because it happens after security and thus after the security's redirected back to my home page.
The before hook with the Application::EARLY_EVENT seems to be to early because as far as I know does the Security provider destroy the session after logout.
Before I keep trying to find a sort of working but probably dirty solution I would like to ask what the best/cleanest solution for this case would be?
UPDATE: After npms hint for a logout event handler I found this article on Google, which describes how to tackle the problem in Symfony very well.
In Silex things are slightly different though and after reading the source of the SecurityServiceProvider I came up with this solution.
$app['security.authentication.logout_handler._proto'] = $app->protect(function ($name, $options) use ($app) {
return $app->share(function () use ($name, $options, $app) {
return new CustomLogoutSuccessHandler(
$app['security.http_utils'],
isset($options['target_url']) ? $options['target_url'] : '/'
);
});
});
class CustomLogoutSuccessHanler extends DefaultLogoutSuccessHandler {
public function onLogoutSuccess(Request $request)
{
$request->getSession()->getFlashBag()->add('info', "Logout success!");
return $this->httpUtils->createRedirectResponse($request, $this->targetUrl);
}
}
The problem however is, that the flashbag message doesn't exist anymore after the redirect. So it seems that the session is being destroyed after the logout success handler is executed... or am I missing something? Is this even the right way to do it?
UPDATE: Still haven't found a proper solution yet. But this works.
I have added a parameter to the target url of the logout and use it to detect if a logout was made.
$app->register( new SecurityServiceProvider(), array(
'security.firewalls' => array(
'default' => array(
'pattern'=> '/user',
'logout' => array(
'logout_path' => '/user/logout',
'target_url' => '/?logout'
),
)
)
));
I had the same problem and your thoughts leaded me to a solution, thank you!
First define logout in the security.firewall:
$app->register(new Silex\Provider\SecurityServiceProvider(), array(
'security.firewalls' => array(
'general' => array(
'logout' => array(
'logout_path' => '/admin/logout',
'target_url' => '/goodbye'
)
)
),
));
Create a CustomLogoutSuccessHandler which handles the needed GET parameters for the logout, in this case redirect, message and pid:
class CustomLogoutSuccessHandler extends DefaultLogoutSuccessHandler
{
public function onLogoutSuccess(Request $request)
{
// use another target?
$target = $request->query->get('redirect', $this->targetUrl);
$parameter = array();
if (null != ($pid = $request->query->get('pid'))) {
$parameter['pid'] = $pid;
}
if (null != ($message = $request->query->get('message'))) {
$parameter['message'] = $message;
}
$parameter_str = !empty($parameter) ? '?'.http_build_query($parameter) : '';
return $this->httpUtils->createRedirectResponse($request, $target.$parameter_str);
}
}
Register the handler:
$app['security.authentication.logout_handler.general'] = $app->share(function () use ($app) {
return new CustomLogoutSuccessHandler(
$app['security.http_utils'], '/goodbye');
});
The trick to make this working as expected is to use another route to logout:
$app->get('/logout', function() use($app) {
$pid = $app['request']->query->get('pid');
$message = $app['request']->query->get('message');
$redirect = $app['request']->query->get('redirect');
return $app->redirect(FRAMEWORK_URL."/admin/logout?pid=$pid&message=$message&redirect=$redirect");
});
/logout set the needed parameters and execute the regular logout /admin/logout
Now you can use
/logout?redirect=anywhere
to redirect to any other route after logout or
/logout?message=xyz
(encoded) to prompt any messages in the /goodbye dialog.

Resources