Switch with no breaks vs switch with breaks on Stripe Webhook - php

So, I have a Stripe webhook for a subscription site for one of my clients and I am using Stripe for the payment process. Because it was faster in my case to use the Stripe PHP SDK directly, what I did is the following preparation steps.
I have created the subscription plans manually in the Stripe dashboard.
I have a database table with specific subscription options and I've copied the corresponding Stripe Subscription Plan IDs to the table.
This way I know which plan has been selected and can make the correct correspondence/relation between customer and subscription type.
Then, the process I have is:
Customer selects the subscription type desired.
I prepare a checkout session using the PHP SDK and send the customer to Stripe to complete the subscription process.
Customer completes the subscription process and comes back to the site to a 5 second "Wait for the subscription to be created" section. This is literally a page with a setTimeout that redirects the customer after 5 seconds to the subscription section and gives the webhook time to do its job.
During those 5 seconds if everything goes well, the webhook is notified of the subscription, creates the transaction and then the subscription. The thing is that sometimes it works and sometimes it doesn't work. If I re-send it, it works, but the idea is not to do this manually.
My question is the following, I am using 3 events for the webhook:
customer.subscription.created
charge.succeeded
customer.subscription.updated
In the end I only end up using the charge.succeeded and the customer.subscription.updated events, because they are the ones I've found useful.
According to the webhook dashboard, the three events were sent and correctly received, but it would seem something's not working on the other end.
Now I am also not sure how this works, because of what my webhook does. The original code the Webhook generator gives me has the events like this:
switch ($event->type) {
case 'charge.succeeded':
$charge = $event->data->object;
case 'customer.subscription.created':
$subscription = $event->data->object;
case 'customer.subscription.updated':
$subscription = $event->data->object;
// ... handle other event types
default:
echo 'Received unknown event type ' . $event->type;
}
But I had to add breaks after each case because in some cases, variables I needed didn't exist in the context of another event.
My question is the following, does the switch with no breaks execute each case in order or only the events it receives, and what would be a good practice in this case to ensure I always receive the information required.
I thought of creating the subscription before the payment, but that just adds extra steps, because if the person decides to go back before paying, I'd have to delete it.

If you use Checkout, you should only be listening to the checkout.session.completed Event instead which indicates the Checkout Session was completed successfully. This is also what Stripe covers in their documentation about fulfillment.
Once you get that Event, you can look at the Session, find the id of the Customer (cus_123) and Subscription (sub_123) associated with that Session and save all the information in your database.
Additionally, event delivery to your webhook endpoint can fail or be slow. Those can happen for various reasons and Stripe has a retry logic documented here.
Since the customer is redirected to your success url, you should already be able to write code to check who they are. This is usually done from a cookie and then you look up their Session id cs_live_123. Alternatively, you can configure Checkout to put the Checkout Session id in the URL as a query parameter which is documented here. This would allow you to synchronously look up the Session, check its completion state, store all the information you need (by re-using the code used in your webhook handler) and synchronously redirect them to the right page. Or you can also poll your database until you got the Event.
It's important to always have a webhook endpoint to catch those events. The customer could close the tab or lost the connection and never be redirected. But if the redirect succeeds, you can already look them up more directly.

Related

GetTransactionDetails without transaction id

I have website where I use PayPal NVP sandbox. Sometimes after successful payment of user, PayPal get back to my return URL very slow. Unless PayPal returns to my website with details I am unable to check whether user has completed the order (For all attempt website adds order id to the database) or not and that destroys website's work principle.
What I need is that I want to retrieve the data of transaction without transaction id if possible. Maybe with custom field or something else. If I would be able to check the transaction details without transaction id , then I would be able to handle the requests by user accordingly. Or if there is a way to get a transaction id somehow with the help of another variable that also fine as I will make checking after finding out the transaction id.
Current PayPal Checkout integrations give an immediate response of success/failure. There is no need for any redirect away from your site, and there is no need to wait for asynchronous IPN or webhook messages. Don't use such things.
Follow the PayPal Checkout integration guide and make 2 routes on your server, one for 'Create Order' and one for 'Capture Order' (see the optional step 5 in 'Add and modify the code'; you can use the Checkout-PHP-SDK). Both of these routes on your server should return/output only JSON data (no HTML or text). Inside the 2nd route, when the capture API is successful you should store its resulting payment details in your database (particularly purchase_units[0].payments.captures[0].id, which is the PayPal transaction ID) and perform any necessary business logic (such as sending confirmation emails or reserving product) immediately before forwarding your return JSON to the frontend caller.
Pair those 2 routes with the frontend approval flow: https://developer.paypal.com/demo/checkout/#/pattern/server

Stripe checkout event for failed payments

I have a paid membership site which takes payments via Stripe Checkout (server integration). When a user registers, they are redirected to the Stripe checkout (with their ID passed as the 'client_reference_id') where they enter their card details. For the checkout fulfillment, I have a webhook setup that is linked to the event 'checkout.session.complete'.
Everything is working fine, the webhook triggers and the endpoint processes the data and marks the corresponding account as activated, sends an email to the account holder as well as the site admin to let them both know the account has been created and a payment has been successful.
However, if a payment fails, I don't know what event to attach to the webhook to detect failed payments via Stripe Checkout. The only checkout event documented is 'checkout.session.complete', there isn't one for something like 'checkout.session.failed'. There's 'invoice.payment_failed' but this is used in subscriptions not one-off checkout payments.
We're wanting to basically notify the site admin when a payment fails so they are aware an account has been created but it still awaiting payment.
Is there a checkout event I'm missing or another way to go about finding failed checkout transactions?
To charge a credit or a debit card, you create a Charge object. You can retrieve and refund individual charges as well as list all charges. Charges are identified by a unique, random ID.
-- From the Stripe API Documentation.
You can then detect a webhook response of charge.failed to show that an attempted use of a charge object has failed.
Stripe used to be very cleanly documented, but they've lost their way a little, recently.
Stripe card payments ALWAYS use Charge objects, sometimes these are set by you (invoices, subscriptions, etc.) and sometimes these are set behind the scenes by Stripe ("Checkout Process", etc) -- but they are always set, so your webhook can always detect a charge.failed event.
Addendum
This does work, however, since the client_reference_id is not in the charge.failed data, I can't link the failed payment to a specific account anyway. But in regards to my actual question, this event does work and is the correct answer, just unfortunate that it doesn't solve my specific situation.
To solve this; either using the Stipe interface or by coding:
1) Create a Customer object and keep some sort of record of this customer Id.
2) When the Stripe Charge runs, an associated $charge->customer is set. This is available to your webhook.
3) When the webhook result appears, use this (typically) $event->data->object->customer value to cross reference the charge with the correct customer on your server.
4) As mentioned in comments, Stripe Webhook Testing does not populate the webhooks with example data such as ->customer... :-(
I know this is a bit late for a reply - but I have a working solution!!
When I create a "Session" in Stripe, I make sure to fill out the SessionCreateOptions so that the PaymentIntentData.Metadata field is setup to have my customer's ID and stuff in it. When I do that, the metadata not only shows up in payment_intent.created and payment_intent.succeeded webhooks, but it ALSO shows up in charge.succeeded/failed!
This means that when a payment fails I can look up the attempted payment that I keep track of and email them back letting them know that it failed and why, and I can also log it for myself for reference when I go look up that customer in my customer service tools.
It's not clear from the documentation and I spent a lot of time grokking how it works, so I hope this will save you some time.
The checkout session CANNOT fail (!)
A Checkout Session can have one of 3 states: "open" is an active session which can be paid, "completed" is paid ("happy path"), and "expired" if within the expiration time the session was not completed.
A session can only expire with two options:
After 24 hours if not paid (or sooner, if you set a shorter expiration time https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-expires_at)
Manually by calling https://stripe.com/docs/api/checkout/sessions/expire
A session's URL can be accessed unlimited number of times for payment attempts until it is either paid or expires. This is by design, so a user can have a couple of failed attempts to pay until she actually pays. With a session, you do not care about those attempts, and in most cases you do not need to track them.
So one option for your workflow can be the following, for example:
For creating a new order, create a new session, save it's ID in the order's record in the DB, and redirect the user to the session's URL
On the back-end, listen for "session completed" event, which is the happy path - everything is paid (watch out for the async payments, if you accept any! See herehttps://stripe.com/docs/payments/checkout/fulfill-orders#delayed-notification)
Also listen for "session expired" event. This will tell you that the user bailed and hasn't paid the order. You can send them a "failed payment, try again" email in this case (see below for Recovery workflow)
If the user returns to your website before paying for the order, you can display "NOT PAID, TRY AGAIN" message next to the unpaid order.
Attention: it is perfectly safe for the user to open a dozen of the session's URL and attempt to pay as Stripe won't allow the double payment after card information is submitted.
So you are thinking about creating expiring the old session and creating a new one for this unpaid order? Wrong! You can retrieve it by ID (you saved it in the DB, right?) and send the same URL to the user.
However, if the retrieved session is "expired", there will be no URL to send, so you will be forced to create a new session, save it in DB, and the whole cycle repeats.
Another possibility is for the repayment workflow to happen in rare state between the payment and "session is completed" webhook event (the user already paid in another tab, but you haven't received the "paid" event yet), you will get that the retrieved session state is "completed", in this case you will need the capability for the front-end not only to redirect the customer to a session's URL, but instead sometimes show a message like "nope, it's already paid".
As you can see, there is no "failed" state for the checkout session.
Now, another thing for recovering the unpaid and expired sessions is the Recovery workflow (https://stripe.com/docs/payments/checkout/abandoned-carts). The gist of it is the following:
When creating a session, you set "recovery" to true, so if the session expires, you will have access to a special URL that will "revive the same session with the same parameters" that will be active for 30 days after the original session expiration. You then send this URL to users with possibly some discount to recover their abandoned cart. Also, you can track whether the session is "original" or "revived". However, I did not use this way, and instead opted to simply creating a new session manually if the old one is expired.

How to validate a sale with PayPal REST API and IPN

I currently have a fully working cart and checkout process through PayPal _xcart method, but I want to migrate it to REST API, mainly because I want to mitigate the possibility of price-jacking. Currently my IPN does check for price jack and sets the according flags so the product doesn't get downloaded (selling digital products only). Anyway more to the point, I found the PayPal documentation very confusing and I'm struggling to get the full grips of it.
This is what I have understood and worked out till now.
Using my PHP script (let's call it page A) I create the cart content, then I create a new PayPal sale and redirect the client to PayPal for authentication
Client authentication on PayPal, then it's redirected back to my site to page B (page B is defined in page A)
Page B needs to get the PaymentID (from page A) and use it to effectively complete the transaction. Once competed finalize the checkout.
Now here are my problems:
a) I had read quite a few forums and tutorials and they all mention that I should use a session to store the PaymentId from Page A and then use it in Page B to finalize the transaction. Some threads on SO suggests that PayPal should actually include the PaymentID in the call to page B, along with the token and the PayerID. Those are almost 3 years old posts and during my testing I see that PayPal now does return the PaymentID as well.
Is this a new thing, did PayPal really started to send the PaymentID as a GET variable? OK I found this on PayPal SDK documentation (not PHP of course some other language) that they return the PaymentID as well as a GET.
Is there any disadvantage on using the GET presented by PayPal compared to storing a session from page A to B? I don't really wan't sessions, so GET will be ideal for me. I guess if it is on PayPal documentation it's safe to use.
Will this work on the live page as well or only in Sandbox?
b) On Page B when I execute the Payment I get a nice JSON as response, but in the same time my IPN listener also gets called, and this is what confuses me big time. Can/Should I just trust all the data which is in the JSON response and more or less ignore the IPN listener? This will make sense for instant download, for example, much more easier to process, or should I still rely on the IPN for data validation?
If I use only the JSON returned to Page B, which are the correct fields to look for and what values? For example, there is a state field which is approved and another (Transactions -> related items -> state) which is competed. Which one do I need to check?
If I rely on the JSON do I still need to check if the paid amount matches the original amount or can I trust that the payment is equal to the amount I have requested in my call?
If I use the IPN how can I pare it with the transaction? The PaymentID doesn't show up in the variables posted to the IPN. The only way I could think of is to get the txn_id from the JSON response, but somehow that feels odd, plus how can I know if the JSON response hit the server BEFORE the IPN?
Can/Should I just trust all the data which is in the JSON response and more or less ignore the IPN listener?
Yes, and no. In that order.
In a nutshell, you can't trust the payment ID in the call to "Page B" (it could be forged, faked, repeated etc) but you can trust the response YourServer->PayPalServer as it can't be intercepted and faked by the end user.
So your process is (as you describe above)
Page A: Create a sessionID (cookie), amount, cart details etc and store in a local database/storage. You can also create a "custom" field to store your own saleID
Send off amount etc to Paypal, which returns you to ....
Page B: Grab the PayPal TransactionID and send back (server->server) to PayPal. Paypal returns the amount, state etc. Then check your database that the amount is the same and that it belongs to the sessionID. If you also use custom fields, check that too. If everything marries, you're good. If not, it's up to you how to handle.
The status should be "complete" for a simple sale at this point; but (as with IPN below) you should verify this.
Do check the amounts, just in case. They should match, but if not, the PayPal one will be what you receive and it's up to you to accept it, flag it (and phone up) or refund through the API and reject the order etc.
So why have IPN?
It is possible that the user completes the transaction on Paypal and then closes their browser before "Page B" is called. In this case, the only way you know about the order is through the IPN.
If you get an IPN notification going to your IPN handler, IPN can still be faked, but there's a slightly different way of verifying.
You actually send the IPN information back to Paypal (server to server) and Paypal confirms it's correct or wrong (https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNIntro/). Your IPN handler will then check the transaction ID (yes, that's what you use) and verify everything matches in the database (just like you do in "Page B"). If it does, mark the order as complete if the status is complete (and if not already marked as complete in "Page B").
Obviously you can't display anything to the user at this point as they are not the ones that called the page.
The docs above warns you that you can get multiple IPNs for the same transaction so you also need to check status.
(Note: you could use the APIs to verify the transactionID as you do in "Page B").
So why not reply on IPN?
Paypal warn that the IPN may not arrive. Paypal explains it best:
Although PayPal usually processes IPN messages immediately, IPN is not synchronized with actions on your website. Internet connectivity is not always 100% reliable and IPN messages can be lost or delayed. The IPN service automatically resends messages until the listener acknowledges them. The service resends messages for up to 4 days.
Because IPN is not a real-time service, your checkout flow should not wait for the IPN message before it is allowed to complete. If the checkout flow is dependent on receiving an IPN message, processing can be delayed by system load or other reasons. You should configure your checkout flow to handle a possible delay.
So back to the original question
Yes: rely on the JSON (server->server) call you make to verify the parameters to "Page B" (and in and the IPN handler if you choose)
No: Don't ignore the IPN in case Page B never gets called. But still run the verification checks here too.
Yes: Check state = complete for both "Page B" and "IPN handler"
Yes: Use the Paypal TransactionID, but mix into your own database with either custom fields or sessionID.
Yes, you can/will get both Page B and IPN notifications, I'd suggest ignoring the IPN if the payment is already marked as completed, otherwise process and handle appropriately. They should be using the same database.

PayPal REST API order workflow: Payment -> Sale -> Webhook?

I am trying to integrate the PayPal REST API into my Symfony 2 web app but I find hard to understand how exactly the complete workflow looks like:
The PayPal docs describe the following steps to accept a payment. One can use the PayPal Playground to simulate these steps:
Get an access token
Create a Payment object by querying the API
Redirect the user to the approval url received in the Payment response
After the user approved the payment on the PayPal page, he is redirected back to my page, using the success-link defined in the Payment object. Use the received information to execute the payment.
Payment is completed with status approved
From the docs: Once a payment is complete, it is referred to as a
sale. You can then look up the sale and refund it.
So far so good. BUT: Where are Webhooks used/fired in this workflow? I have defined a wildcard Webhook (accepting all possible events) in the PayPal Developer Dashboard.
My observation is, that my system receives the Webhook event 1-2 Minutes (!) after the user was redirected back to the success-link and after the payment was executed (Step 4).
Beside this long delay between executing the payment and receiving the Webhook, this workflow means, that I only receive the Webhook AFTER handling the success-link. This means, handling the success-link is absolutly necessary for the payment to be completed. Is this correct?
Do I need to use Webhooks?
I already asked this question a few days before and the answer by nifr is quite reasonable: One cannot trust the user to follow any redirect URL but should only rely on the Webhook events.
However this collides with the observations I described before, since I will never receive the Webhook without handling the redirect URL...
So, handling the PAYMENT.SALE.COMPLETED webhook event does not make a lot of sense, since this should already be done in when handling the redirect URL. Correct?
However, to handle updates on pending payments, handle refunds or reversed payments, etc. are only possible by listening on those events.
So the answer is: Only use Webhooks to get updates on payments made before. Correct?
So, the main questions are:
The 5-step process to accept payments does not say anything about using Webhooks. This does not seem to make a lot of sense, because without Webhooks one would miss update events, etc.? So, is it really possible to implement the complete payment workflow without Webhooks?
If yes, how are updates (refunds, pending, etc) handled in this case?
If no, what is the right strategy/time to fulfill the order since it take quite a long time to completly receive and handle the webhook?
i am still a newbie in PayPal world, but few days ago i integrated PayPal Plus REST API in an online Shop, and from my understanding i can tell that the workflow looks like:
create a Payment
redirect to PayPal
Payer could pay using PayPal account OR (using Bank Direct debit or Credit Card Payment without PayPal Account)
After completing the process on PayPal side, PayPal redirect the user back to your success URL.
till now the user is still not charged(you got no money). At the moment where you (in your success URL) do $payment->execute($paymentExecution,$api); , you ask Paypal to charge the amount from user. BUT also after this, you got no Money. Paypal have first to process the charging and notify you later via WebhookEvents.
the Webhook Notification (with that nasty delay) is especially important when the user pays per direct debit or Credit Card etc. Processing such Payments takes few seconds/minutes.
the redirectUrl ist absolutly necessary for charging/executing the Payment.
here on execution succeed, just to tell the user, that he finished his Job, and you can here save/capture the PaymentID/Transaction id for later usage/update via WebhookEvent Listener.
so i would recommend you to update your Database(Payment completed) only after receiving notofications via WebhookEvent Listener and not in the success RedirectUrl.

Way to check if customer has made a payment?

I'm trying to find a way of checking if a customer has clicked the final submit button to perform a transaction in PHP.
I need to send a second e-mail to different people when the customer makes a transaction, but as far as I can tell, there's no function or property to retrieve some sort of confirmation value.
I don't need to know if the payment went through or not, just if they clicked the last submit button, but the checkout page is hosted on Microsoft's servers.
All of the payment APIs will return a status immediately. You just have to look for it and handle it in your code. How this is done will vary by API.
You can use Silent Post* which is similar to Paypal's IPN. Basically a script on your server will be notified of all payments.
Use the Transaction Details API and look for the transaction in your unsettled batches.
* I am the author of that article

Categories