paypal / PayPal-PHP-SDK

PHP SDK for PayPal RESTful APIs
https://developer.paypal.com/docs/api/
Other
25 stars 95 forks source link

Webhooks #1189

Closed daslicht closed 5 years ago

daslicht commented 6 years ago

Hi, I implemented a WebHook listner as it is shown here:

https://paypal.github.io/PayPal-PHP-SDK/sample/doc/notifications/ValidateWebhookEvent.html

Dont that Listner need to retuern anything so that the PayPal server knows that the message has been delivered?

When I send a WebHook event in the Simulator its state is shown as pending.

~ Marc

neokyuubi commented 6 years ago

@daslicht I faced the same thing, and the problem i were unable to find any clue in the docs/samples, but i found in this tutorial that you need to echo the $status at the end, and even so some times the webhook still pending (Orange color) as you can see here : https://github.com/paypal/PayPal-PHP-SDK/issues/1186

daslicht commented 6 years ago

I have meanwhile created some tests, when I do a sandbox payment , anything is ok. But I still wonder how often a WebHokk will be called ?

neokyuubi commented 6 years ago

Did you add that echo $status at the end ?

$status = $output->getVerificationStatus();
echo $status;

Without it i couldn't have any green-check-mark/non-pending status in the webhook's dashboard

daslicht commented 6 years ago

At the moment I have something like this without the echo http://paypal.github.io/PayPal-PHP-SDK/sample/doc/notifications/ValidateWebhookEvent.html

So is it missing in the sample ?

daslicht commented 6 years ago

here something funky, my WebHook Listener doent retuen anything, but i still see some green things here :

daslicht commented 6 years ago

Just found this here : https://gitter.im/paypal/paypal-checkout

daslicht commented 6 years ago

As far as i know verifiying a WebHook only works on a real SandBox paymen and not a simulation. It seams when verifying a WebHook Event, a post request is send to the paypal server: https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature

Probably PayPal get that way feedback ?

neokyuubi commented 6 years ago

Ah ok so the simulator send only a dummy date then ! Yes we need to send a post back to paypal to verify and $output->getVerificationStatus(); is doing that.

So even if i remove the entire code of verifying and echoing the status, i may receive a green one !!! so i assume that at the first time i did not try to much to see if i can catch a green one,

But the confusing thing is the payment is done whether it's green or orange ! i guess it's the same thing for you ? payment is done no matter what it says in the dashboard ?

daslicht commented 6 years ago

Yeah, payment went through even without any webhook at all :) The WebHook events are not tightly bound to the Payment process itself, it is just a notification.

The pending state of the Webhook semas to indicate that the WebHook hasnt been deliverd yet, but the Payment itself shows "completed".

Essentially I just use the Webhook to handle the "pending" state of an "sale"

Sebbo94BY commented 6 years ago

Thank you for this issue. It already helped me a lot to integrate the webhook.

@daslicht I faced the same thing, and the problem i were unable to find any clue in the docs/samples, but i found in this tutorial that you need to echo the $status at the end, and even so some times the webhook still pending (Orange color) as you can see here : #1186

I've done the same like in the tutorial, mentioned by @Neokyuubi , but my application doesn't get any incoming webhook using the Sandbox. Do I need to add the webhook listener also to the public function webhooksPaymentSaleCompleted(Request $request) { ... } in order to receive any webhooks?

The PayPal developer dashboard shows, that every webhook was successful, even when I do not echo the $status, but I've added a logging to my webhook and it doesn't get triggered. There is no POST request incoming at all. :(

I thought, I may have enabled the wrong event types in the developer portal, but I've also tried it with the wildcard setting (just all events).

Any idea, why the webhook isn't reaching my application? The application is reachable via internet and HTTPS.

daslicht commented 6 years ago

Hi, You I am using the FatFreeFramework so I cant comment pn the Laravel based tutorial.

Have you checked if your webhook post route works with a simple form submit or postman ?

daslicht commented 6 years ago

My implementation is working meanwhile, even the pending state :)

Sebbo94BY commented 6 years ago

Yes, the route is working fine. When I trigger the route manually, it also logs something, but the webhook isn't even reaching the route, so it's not triggered at all. :(

daslicht commented 6 years ago

You enabled the webhook at the developer dashboard ?

daslicht commented 6 years ago

How about sending sandbox mock events in the dashboard? What does it log say ?

neokyuubi commented 6 years ago

@Sebi94nbg Your address start with https, isn't it ? if not, then you need to have https in order to work.

You're using laravel ? if yes, are you sure you route is Route::post('yourpagename', 'YourController@webhooksPaymentSaleCompleted') ?

if still does not work move that route from web.php to the api.php file under routes folder, so the address that need to be passed in the dashboard becomes : https://yourwebsite.com/api/yourpagename

Sebbo94BY commented 6 years ago

Yes, the webhook is enabled on the developer dashboard as you can see here: PayPal Dashboard App Settings

The Sandbox webhook events are always empty. I would expect, that there are some listed.

However, the API call history shows always "success" for every payment - even, when I do not echo $status: PayPal API Call History

The mock webhook simulator returns always, that it was successful, but the webhook function in my Laravel application does not log anything, although I've added to the first line to write "Incoming webhook..." to the Laravel log: PayPal Mock Webhook Simulator

@Neokyuubi I've already configured everything as API request in the API route. :)

That's my routes/api.php route: Route::post('/payment/paypal/hook', 'Helpers\Payments\PayPalController@webHook')->name('api.payment.paypal.hook');

And this is the called method Helpers\Payments\PayPalController@webHook:

<?php
public function webHook(Request $request)
{
    Log::critical("Incoming webhook...");
    // Get request details
    $request_body = $request->getContent();
    $headers = array_change_key_case($request->headers->all(), CASE_UPPER);

    // Verify webhook signature
    $signature_verification = new VerifyWebhookSignature();
    $signature_verification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO'][0]);
    $signature_verification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID'][0]);
    $signature_verification->setCertUrl($headers['PAYPAL-CERT-URL'][0]);
    $signature_verification->setWebhookId(config('payment-methods.supportedPaymentMethods.paypal.webhook_id'));
    $signature_verification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG'][0]);
    $signature_verification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME'][0]);
    $signature_verification->setRequestBody($request_body);
    $req = clone $signature_verification;

    try {
        $output = $signature_verification->post($this->apiContext);
    } catch (\Exception $ex) {
        Log::critical($ex->getMessage());
        return response('Error: Could not verify signature.', 500)->header('Content-Type', 'text/plain');
    }
    $status = $output->getVerificationStatus(); // 'SUCCESS' or 'FAILURE'

    switch(strtoupper($status)) {
        case "FAILURE":
            return response('Forbidden: Invalid signature.', 403)->header('Content-Type', 'text/plain');
        case "SUCCESS":
            $json = json_decode($request_body, 1);

            // Because PayPal don't let us to add in custom data in JSON form, so I add it to a field 'custom' as encoded string. Now decode to get the data back
            $custom_data = json_decode($json['resource']['custom'], 1);
            $user = User::find($custom_data['user_id']); // to get the User

            // Update the payment info
            Log::critical(serialize($json));

            return response($status, 200)->header('Content-Type', 'text/plain');
    }

    return response('Error: Invalid webhook.', 500)->header('Content-Type', 'text/plain');
}

I've even told my app/Http/Middleware/VerifyCsrfToken.php to ignore missing Csr tokens:

protected $except = [
    '/api/payment/paypal/hook',
];

After creating the payment, the user is redirected to PayPal, where he confirms / approves the payment. After this, he is redirected back to my application, which is only showing the text "payment successful" or "payment canceled" - it doesn't do anything else with the payment. No execution or something else.

I expect, that the API triggers my webhook URL and this will execute and do the rest of the payment. Or do I need to execute the payment, when the user gets redirected to my page?

The reason, why the redirect doesn't do anything yet is that as the user may is not redirected, because he may just closed the window after approving the payment. In this case, the payment execution will never be executed and due to this, I expect a notification by PayPal about a status change.

Sebbo94BY commented 6 years ago

I've updated my code to get it working now.

The user needs to be redirected to the success_url and then, the payment needs to be executed. Only after this execution, the webhook will be triggered.

I've updated my webhooks method to this:

<?php
public function webHook(Request $request)
{
    // Get request details
    $request_body = $request->getContent();
    $headers = array_change_key_case($request->headers->all(), CASE_UPPER);

    // Verify webhook signature
    $signature_verification = new VerifyWebhookSignature();
    $signature_verification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO'][0]);
    $signature_verification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID'][0]);
    $signature_verification->setCertUrl($headers['PAYPAL-CERT-URL'][0]);
    $signature_verification->setWebhookId(config('payment-methods.supportedPaymentMethods.paypal.webhook_id'));
    $signature_verification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG'][0]);
    $signature_verification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME'][0]);
    $signature_verification->setRequestBody($request_body);
    $req = clone $signature_verification;

    try {
        $output = $signature_verification->post($this->apiContext);
    } catch (\Exception $ex) {
        Log::critical($ex->getMessage());
        return response('Error: Could not verify signature.', 500)->header('Content-Type', 'text/plain');
    }
    $status = $output->getVerificationStatus(); // 'SUCCESS' or 'FAILURE'

    switch(strtoupper($status)) {
        case "FAILURE":
            return response('Forbidden: Invalid signature.', 403)->header('Content-Type', 'text/plain');
        case "SUCCESS":
            $json = json_decode($request_body, 1);
            goto UPDATE_TRANSACTION;
    }

    UPDATE_TRANSACTION:
    $state = $json['resource']['state'];
    $reference_id = $json['resource']['parent_payment'];

    switch($json['event_type']) {
        case "PAYMENT.AUTHORIZATION.CREATED":
            // A payment authorization is created, approved, executed, or a future payment authorization is created.
        case "PAYMENT.AUTHORIZATION.VOIDED":
            // A payment authorization is voided.
        case "PAYMENT.SALE.PENDING":
            // The state of a sale changes to pending.
        case "PAYMENT.SALE.DENIED":
            // The state of a sale changes from pending to denied.
        case "PAYMENT.SALE.REFUNDED":
            // A merchant refunds a sale.
        case "PAYMENT.SALE.REVERSED ":
            // PayPal reverses a sale.
        case "PAYMENT.SALE.COMPLETED":
            // A sale completes.
            if($state !== "completed") {
                    return response('Forbidden: Payment is not completed yet.', 403)->header('Content-Type', 'text/plain');
            }

            if(!Transactions::where('reference_id', '=', $reference_id)->update(['paid' => 1])) {
                    return response('Error: Could not update database entry.', 500)->header('Content-Type', 'text/plain');
            }

            return response($status, 200)->header('Content-Type', 'text/plain');
        case ("RISK.DISPUTE.CREATED" || "CUSTOMER.DISPUTE.CREATED"):
            // A customer dispute is created.
        case "CUSTOMER.DISPUTE.UPDATED":
            // A customer dispute is updated.
        case "CUSTOMER.DISPUTE.RESOLVED":
            // A customer dispute is resolved.
    }

    return response('Error: Invalid webhook.', 500)->header('Content-Type', 'text/plain');
}

This should handle all all necessary API requests by PayPal regarding a payment. Code needs to be added to each case depending on the own requirements and preferences. The case PAYMENT.SALE.COMPLETED has already an example code to update a created transaction to "paid".

daslicht commented 6 years ago

I only use the webhook for the "pending" state of a sale, If it is completed I dont use the webhook at all.

neokyuubi commented 6 years ago

@Sebi94nbg Good ^^,

@daslicht but when the $state is "pending" and you look into the buyer and seller accounts don't you see that the amount of the money has been transferred ?

Sebbo94BY commented 6 years ago

I only use the webhook for the "pending" state of a sale, If it is completed I dont use the webhook at all.

For what is this useful, @daslicht ? Usually, you want to know, if the payment is fully completed and you have received the money or not. :D

daslicht commented 6 years ago

@daslicht but when the $state is "pending" and you look into the buyer and seller accounts don't >you see that the amount of the money has been transferred ? I just save the order with all its data and flag it as pending :)

daslicht commented 6 years ago

I only use the webhook for the "pending" state of a sale, If it is completed I dont use the webhook at all.

For what is this useful, @daslicht ? Usually, you want to know, if the payment is fully completed and >you have received the money or not. :D

Here is the flow:

  1. Paypal Button (clientside)
  2. createPayment, called from clientside create payment callback
  3. executePayment, called from the onAuthorize callback client side
  4. createOrder, called from executePayment

So If we have now a completed state in the executePayment function, we can immediatelly ship the digitral goods without waiting for the webhook to be called. For example you could immediately return a downloadlink to the clinet siede JS callback.

However if the payment status is pendinding , it might be a less good idea to ship you product before getting the money for sure.

btw the PayPal tech support told me that under some circumstances it could take several days... (eg some bank transfer etc...)

In case if a transaction is pending , I save the Order as regular but flaged as pending.

Once the Webhook calls the api, I search for the related order, flag it as completd and ship the product.

So why do I do not use the WebHook if a sale is completred? Because I like to ship the product immediately and not let the customer wait for teh webhook gets called.

daslicht commented 6 years ago

I only use the webhook for the "pending" state of a sale, If it is completed I dont use the webhook at all.

For what is this useful, @daslicht ? Usually, you want to know, if the payment is fully completed and you have received the money or not. :D

pending means that you should receive the money but you havent received it yet. If it is complted you already have received the money. So why let the customer wait for a webhook when you already know that you have the money ?

Sebbo94BY commented 6 years ago

Makes sense, @daslicht! I'll just update my code for payments. :D

Thank you for the feedback. :)

daslicht commented 6 years ago

I am happy that my spend time (yeah it was looooonng) helped somone else :)

Sebbo94BY commented 6 years ago

I've integrated multiple payment solutions in my application, I know how much time you can spent on such things. :D

daslicht commented 6 years ago

Yeah me to, I tried alot gateways and even ready drop in carts.

neokyuubi commented 6 years ago

@daslicht O_o so i was getting wrong information from my test accounts in sandbox ? In my case even if the state is pending in the webhook i see that the buyer account money has been reduced and the seller increased... i'm really confused here !

daslicht commented 6 years ago

@Neokyuubi We have a sales state & the WebHook state.

Here a mock Webhook log:

{
    "id": "WH-84454924Y2472560V-5CSSSSSSSN865151V",
    "create_time": "2018-09-24T08:32:08.418Z",
    "resource_type": "sale",
    "event_type": "PAYMENT.SALE.COMPLETED",
    "summary": "Payment completed for EUR 1.0 EUR",
    "resource": {
        <<<snip>>>
        "state": "completed", <--------SALE
        <<<snip>>>
    },
    "status": "PENDING", <--------WEBHOOK
    <<<snip>>>
}
daslicht commented 6 years ago

Might be interesting, If you notalready found it:

Production webhook events can be found within PayPal Developer in the "LIVE" section, under "Webhook Events": https://developer.paypal.com/developer/dashboard/webhooks/live/

neokyuubi commented 6 years ago

Does that mean PAYMENT.SALE.PENDING is not reliable ? (it says The state of a sale changes to pending here : https://developer.paypal.com/docs/integration/direct/webhooks/event-names/#sales)

daslicht commented 6 years ago

hm thats the Sales state, the money is even transfered when it says PAYMENT.SALE.PENDING ?

neokyuubi commented 6 years ago

I think no, i have never received this one. But the thing is some times when i receive PAYMENT.SALE.COMPLETED, i still can see pending in the payment state, i was thinking that in the case where the payment is pending i should receive PAYMENT.SALE.PENDING event not PAYMENT.SALE.COMPLETED with sale state pending !

Sebbo94BY commented 6 years ago

You're driving me crazy. Now I am no longer sure if my Webhook code logic is correct or not. :D

daslicht commented 6 years ago

I think no, i have never received this one. But the thing is some times when i receive PAYMENT.SALE.COMPLETED, i still can see pending in the >payment state, i was thinking that in the case where the payment is pending i should receive >PAYMENT.SALE.PENDING event not PAYMENT.SALE.COMPLETED with sale state pending !

where do you "see" iot pending , what does the API say ?!

daslicht commented 6 years ago

You're driving me crazy. Now I am no longer sure if my Webhook code logic is correct or not. :D

If it is working , why ?

daslicht commented 6 years ago

One important thing to mention is that you like to set the intent to SALE:

$payment->setIntent( 'sale' )
$sale = $resources[0]->getSale();

        /**
         * CHECK PAYMENT STATUS
         * Valid Values: 
         * [**"completed"**, "partially_refunded", **"pending"**, "refunded", "denied"]
         */
        $state =  $sale->getState();

Thts the important part, either you have the money here or not :)

Sebbo94BY commented 6 years ago

Yeah, it's working fine. I need to ask PayPal to verify the integration once to make sure, that I really handle all cases correctly. :)

The PayPal Webhook ends at this piece of code in my application:

$request_body = $request->getContent();
$json = json_decode($request_body, 1);
$state = $json['resource']['state'];

switch($json['event_type']) {
    case "PAYMENT.AUTHORIZATION.CREATED":
        // A payment authorization is created, approved, executed, or a future payment authorization is created.
        return response($status, 200)->header('Content-Type', 'text/plain');
    case "PAYMENT.AUTHORIZATION.VOIDED":
        // A payment authorization is voided.
        return response($status, 200)->header('Content-Type', 'text/plain');
    case "PAYMENT.SALE.PENDING":
        // The state of a sale changes to pending.
        return response($status, 200)->header('Content-Type', 'text/plain');
    case "PAYMENT.SALE.DENIED":
        // The state of a sale changes from pending to denied.
        return response($status, 200)->header('Content-Type', 'text/plain');
    case "PAYMENT.SALE.REFUNDED":
    case "PAYMENT.SALE.REVERSED":
        // A merchant refunds a sale
        // or PayPal reverses a sale.
        return response($status, 200)->header('Content-Type', 'text/plain');
    case "PAYMENT.SALE.COMPLETED":
        // A sale completes.
        if($state !== "completed") {
                return response('Forbidden: Payment is not completed yet.', 403)->header('Content-Type', 'text/plain');
        }

        return response($status, 200)->header('Content-Type', 'text/plain');
    case "RISK.DISPUTE.CREATED":
    case "CUSTOMER.DISPUTE.CREATED":
        // A customer dispute is created.
        return response($status, 200)->header('Content-Type', 'text/plain');
    case "CUSTOMER.DISPUTE.UPDATED":
        // A customer dispute is updated.
        return response($status, 200)->header('Content-Type', 'text/plain');
    case "CUSTOMER.DISPUTE.RESOLVED":
        // A customer dispute is resolved.
        return response($status, 200)->header('Content-Type', 'text/plain');
}

Between $request_body = ... and $json = ... it's also verifying the webhooks signature. :)

I've set $payment->setIntent( 'sale' ) as you mentioned, but I'm not sure, if I even need these PAYMENT.AUTHORIZATION´s for this intent.

Regarding disputes: I'm not sure yet, how PayPal expects, that the application should handle disputes. Should I mark the payment from "received" back to "approved" or should I just do nothing?

daslicht commented 6 years ago

I just handle pending opr completed nothing else which is totally fine for my application.

I dont have even thought of disputes yet :)

Sebbo94BY commented 6 years ago

Yeah, this makes the life easier. :D

lyongdee commented 5 years ago

@Sebi94nbg I tracked All Webhook event, and webhook verification is SUCCESS. But I just can receive one "event_type":"PAYMENT.SALE.PENDING", how to resolve it.

Sebbo94BY commented 5 years ago

@lyongdee so you always only get PAYMENT.SALE.PENDING and no other of my above mentioned cases (eg. PAYMENT.AUTHORIZATION.CREATED or PAYMENT.SALE.COMPLETED)?

And you have enabled (ticked) all of those webhook events in your PayPal Developer account under https://developer.paypal.com/?

daslicht commented 5 years ago

you set the intent to sale ?

prakash-gangadharan commented 5 years ago

Hi @daslicht, I hope your query has been answered by @Sebi94nbg , if you still facing any challenges revert us back, closing this issue for now. Please feel free to reopen if you have any concerns.

Thanks!