henrik
henrik

Reputation: 1618

Handle paypal redirect failuers with omnipay paypal

I've implemented Omnipay paypal into my Laravel site. First I do an authorization call to paypal which looks like this:

$invoice = $this->find($id);
$gateway = Omnipay::create('PayPal_Express');
$gateway->setUsername(config('payment.paypal_api_username'));
$gateway->setPassword(config('payment.paypal_api_password'));
$gateway->setSignature(config('payment.paypal_signature'));
$gateway->setTestMode(config('payment.paypal_testmode'));

$response = $gateway->purchase([
    'cancelUrl' => route('paypal.cancel'),
    'returnUrl' => route('paypal.return'),
    'amount' => $invoice->price.'.00',
    'currency' => $invoice->currency->abbr,
    'Description' => 'Sandbox test transaction'
])->send();

if($response->isRedirect()) {
    // Redirect user to paypal login page
    return $response->redirect();
} else {
    Flash::error('Unable to authenticate against PayPal');        
}

After this the user gets redirected to pay in paypal's site and if successful gets redirected back to returnUrl. Where this happens:

$payment = Payment::where('paypal_token', '=', $token)->first();
$invoice = $payment->invoice;
$gateway = Omnipay::create('PayPal_Express');
$gateway->setUsername(config('payment.paypal_api_username'));
$gateway->setPassword(config('payment.paypal_api_password'));
$gateway->setSignature(config('payment.paypal_signature'));
$gateway->setTestMode(config('payment.paypal_testmode'));

$response = $gateway->completePurchase(array (
    'cancelUrl' => route('paypal.cancel'),
    'returnUrl' => route('paypal.success'),
    'amount' => $invoice->price.'.00',
    'currency' => $invoice->currency->abbr 
))->send();

if($response->isSuccessful()) {
    Event::fire(new MakePaymentEvent($invoice));
    Flash::message('Thank you for your payment!');
} else {
    Flash::error('Unable to complete transaction. Check your balance');
}

However occasionally something fails with the redirect back from paypal, perhaps the browser shuts down or a network failure prevents the user from getting redirected back to my laravel site.

So I've tried to create a cron job (scheduler in laravel) which checks every 5 minutes for payments that are not complete and try check them if the payment has been transfered successfully and set the invoice status to payed if so:

$gateway = Omnipay::create('PayPal_Rest');

$gateway->initialize(array (
    'clientId' => config('payment.paypal_client_id'),
    'secret'   => config('payment.paypal_secret_id'),
    'testMode' => config('payment.paypal_testmode'),
));

$transaction = $gateway->fetchPurchase();
$transaction->setTransactionReference($token);
$response = $transaction->send();
$data = $response->getData();
dd($data);

But I only get this response from paypal api

array:4 [
  "name" => "INVALID_RESOURCE_ID"
  "message" => "The requested resource ID was not found"
  "information_link" => "https://developer.paypal.com/webapps/developer/docs/api/#INVALID_RESOURCE_ID"
  "debug_id" => "8240e7d79fa91"
]

So how am I supposed to fetch the payment transaction when all I have is this from the authorization request made before the user gets redirected:

#data: array:6 [▼
    "TOKEN" => "EC-9XF92859YM415352K"
    "TIMESTAMP" => "2016-02-12T14:25:09Z"
    "CORRELATIONID" => "e6a70075ad9d5"
    "ACK" => "Success"
    "VERSION" => "119.0"
    "BUILD" => "18308778"
]

I have tried fetching the transaction with both the token and the correlationid, but none of them are valid resource id's

Upvotes: 1

Views: 1659

Answers (1)

delatbabel
delatbabel

Reputation: 3681

  1. I recommend you switch from the PayPal Express API to the Paypal REST API. The REST API is newer, better documented, and more complete.
  2. I recommend that you provide a unique return and cancel URL for each transaction. An example of doing this is in the omnipay-paypal docblocks for the REST API. Basically you need to store the transaction data and a transaction ID before you call purchase() and use that ID as part of the returnUrl. Then the code in the returnUrl will know which transaction this is for.
  3. Your approach in terms of creating a cron job to search for missing transactions is correct (I do that exact thing myself except I only do it once per day), however you need to search on the transaction reference provided by the initial purchase() call, not on the ones provided in the returnUrl.

So, for example, looking at your initial code, I have added some comments as to the changes that I would make:

    // OK I'm assuming that your $id is a transaction ID, so we will use that.
    $invoice = $this->find($id);

    // Make the purchase call
    $response = $gateway->purchase([
        // Add $id to these two URLs.
        'cancelUrl' => route('paypal.cancel') . '/' . $id,
        'returnUrl' => route('paypal.return') . '/' . $id,
        'amount' => $invoice->price.'.00',
        'currency' => $invoice->currency->abbr,
        'Description' => 'Sandbox test transaction'
    ])->send();

    if($response->isRedirect()) {
        // Get the transaction reference.
        $txnRef = $response->getTransactionReference();
        // Store the above $txnRef in the transaction somewhere.
        // This will be the transaction reference you need to search for.

        // Redirect user to paypal login page
        return $response->redirect();

Note that:

  • $txnRef is the key.
  • There will be some transactions where you have the $txnRef but they can't be found on PayPal. It appears that these are cancelled transactions and they time out after a while. Assume that they are cancelled if not found.
  • There will be some transactions that come back with a different transaction reference in the response data from the PayPal fetchTransaction() call. This usually means that the transactions are complete on the PayPal side and that you need to update your transaction reference.

See the documentation in the omnipay-paypal RestFetchTransactionRequest, RestFetchPurchaseRequest and RestListPurchaseRequest classes for some examples.

Upvotes: 4

Related Questions