How can I prevent duplicate charge using Stripe? - node.js

I'm using Stripe to allow my customer to save a credit card and pay invoices (that have a unique id). On payment, I use a form to send a POST request with the card id (provided by stripe) to my server, and the controller charges it. Then I marked the invoiced as payed on my side.
Turns out that if I double click my form's submit button quick enough, I send two POST request and the cards ends up being charged twice, because when the second POST request arrives on my server, the POST request from my server to Stripe API (from the first click) isn't finished, thus the invoice has not been marked as payed yet.
I'm considering disabling the form submit button after first click, but I feel there may be a cleaner way.
Is there on Stripe's side to prevent duplicating charges (maybe using my invoice id)? If not what would the best back-end method to do this on my side?

Disabling the form submission after the first click is definitely the easiest way to implement this.
Another approach is to use idempotent requests as documented here. The idea is that you pass a unique identifier when making a request to Stripe's API that ensures you can only run this transaction once. If you re-run the query with the exact same idempotency key you will get the original response from the first call which ensures you never have 2 charges created for the same "transaction" on your website.

yes idempotent request is correct way to implement this.
you can refer this here
https://stripe.com/docs/api#idempotent_requests
another simple solution which you can implement is use of state machine by keeping track of stripe api call.

As mentioned by other answers, you can use Idempotent Requests to prevent duplicate charges. Here's how I've implement it:
On checkout.php:
<?php
function rand_string( $length ) {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return substr(str_shuffle($chars),0,$length);
}
$idempotency = rand_string(8);
?>
...
<form method="post" action="payment.php">
<input type="hidden" name="idempotency" id="idempotency" value="<?=$idempotency?>">
...
</form>
...
On payment.php:
<?php
$idempotency = preg_replace('/[^a-z\d]/im', '', $_POST['idempotency']);
...
try {
$charge = \Stripe\Charge::create(
["amount" => $price,
"currency" => $currency,
"description" => $invoiceid,
"customer" => $customer->id ],
["idempotency_key" => $idempotency,]);
}catch(Exception $e) {
$api_error = $e->getMessage();
}

Related

How to check if payment sheet is open?

I'm using React and I have an effect (i.e. useEffect) that tries to update the total in a paymentRequest (i.e. paymentRequest.update).
When I try to do that, stripe throws an error:
IntegrationError: You cannot update Payment Request options while the payment sheet is showing
…which make enough sense but now my issue is that I don't know how to check if the payment sheet is already open or not to prevent the update from being called.
I'm looking to do something like this:
if (!paymentSheetOpen()) {
paymentRequest.update({/* ... */});
}
How do I implement paymentSheetOpen?
I asked this question on the stripe IRC and this was the user timebox's answer:
You need to listen to/for the click and cancel events, per here: https://stripe.com/docs/js/payment_request/update

Stripe: Get card information so customer can update their card

My app uses subscriptions with Stripe.
I want to create a standard "account" page, which will list the customer's current card information (like "MasterCard" and last 4 of card number), and give the customer the option of updating that information.
I'm stuck on the first piece--getting back the current card information. To do this, I need the current card_id. I have tried the "listSources" method, but this returns blank data where the card info is supposed to be. What do I need to do to get that card info?
Here is what I've tried:
(I'm using Node, and running this server side)
The closest method I have found is here:
var stripe = require('stripe')(STRIPE_TOKEN);
stripe.customers.listSources(
CUSTOMER_ID,
{object: 'bank_account', limit: 3},
function(err, cards) {
// asynchronously called
}
);
This returns information (there's no error), but the docs say this method should return a data array of the cards that includes the card id for each. In testing, the data array keeps coming back empty.
I am testing with a customer id that has a valid subscription and a card that I can see on my Stripe dashboard.
Why is the data array coming back empty?
Note: there's also a retrieve source method, which should give back card details, but this method requires you have the id of the card you want info on, and that's what I am not able to get right now.
Converting this to an answer...
Stripe has recently rolled out PaymentMethods, which replace (and are separate from) the older Tokens and Sources API.
OP's issue is that their integration creates PaymentMethod objects, which won't show up in the sources list, but can instead be accessed via stripe.paymentMethods.list.

SilverStripe Multiform and Stripe payment

I'm trying to build a form using multiform module and Stripe payment as the final step. The problem is, I still wish the user can go back to the previous step when they are in the final Stripe payment step. But if I enable can_go_back in the final step, when hitting submit, the form will go directly to the previous step without sending payment to Stripe. When I disable can_go_back (set value to false), the submission and payment work fine.
My guess is the way Stripe works (submit the form => create token => add hidden input with token value => submit again), the two submissions is confusing Multiform module. However, even if I disable can_go_back (so there is only one submit button), and manually add a link to the previous step, it still goes to the previous step without sending payment when hitting submit.
Here is what I used to get the previous step link:
public function PrevLink(){
$prevStepClass = $this->getCurrentStep()->getPreviousStep();
$prevStep = DataObject::get_one($prevStepClass, "\"SessionID\" = {$this->session->ID}");
$this->setCurrentStep($prevStep);
return $prevlink = $prevStep->Link();
}
Anyone has any ideas? Thanks!

Passing sever side zip/postcode to Stripe checkout

I am trying set up Stripe checkout so I can pass my server side customer zip/postcode to the Stripe checkout. It seems like it should be very simple but I cannot get it to work!
At it's simplest, the code I am using is:
<form action="charge.php" method="POST">
<script
src="https://checkout.stripe.com/v2/checkout.js" class="stripe-button"
data-key="pk_test_redacted"
data-amount="2000"
data-name="Demo Site"
data-description="2 widgets (£20.00)"
data-currency="gbp"
data-address_zip="NG15 6UR">
</script>
</form>
the transaction works fine but it does not pass the zip/postcode so that it can be AVS checked.
What am I doing wrong?
Edit:
So apparently you cannot pass address information using checkout.js - you can only get the form to require the address from the customer when they put their card details in using
data-address="true2"
As far as I can tell, this means I can either hassle the customer by getting them to add their address details when I already have them!
or I have to use stripe.js which means I have build my own form, make it look pretty, validate it etc. Not a massive thing but, when there is a form there that does everything I need but accept address form fields passed at form creation, it is a little annoying :-(
If anyone knows otherwise I would love to hear it but this info came from the #stripe irc channel so I think it is probably correct.
As said in the Edit, I did have to use stripe.js and make my own form. Shame this small omission means quite a bit of extra work - but still much better than Paypal's mess of APIs and documentation! Cheaper too.
Untested but there may be a solution as you can update a card before the charge , you are able to update the customers card from the $token that you pass to your backend processor in php for example:
$cu = Stripe_Customer::retrieve($customer->id);
$card = $cu->cards->retrieve($token);
$card->address_city = $form['city'];
$card->address_line1 = $form['address_line_1'];
$card->address_line2 = $form['address_line_2'];
$card->address_state = $form['county'];
$card->address_country = "United Kingdom";
$card->address_zip = $form['postcode'];
Then take the charge
$charge = Stripe_Charge::create(array(
'customer' => $customer->id, etc...

Lingering CartThrob Session

I'm running into a weird issue with CartThrob. Googling and CartThrob forums haven't revealed the answer.
After the cart is sent to the payment gateway, it will return to the template a "state" of either "authorized", "processing", "declined" or "failed".
What I'm finding is this "state" lingers around after browser refreshes, including full (no cache) browser refreshes.
This is mostly an issue with the "authorized" message. The "authorized" message appears when an order has been 100% completed.
What I'm seeing is if I start another order right away and go to the template where this code lives, the "authorized" message is still there. The message eventually goes away... maybe after 10/20 minutes or so. But it should go away immediately in my opinion, right? The order is done. Clear everything.
Is this "state" stored in the CartThrob session? Can I force clear the CartThrob session?
{exp:cartthrob:submitted_order_info}
{if authorized}
Order complete!
{if:elseif processing}
Your order is being processed!
{if:elseif declined}
Your credit card was declined: {error_message}
{if:elseif failed}
Your payment failed: {error_message}
{/if}
{/exp:cartthrob:submitted_order_info}
If you only want this info to show up on the post-checkout page the simplest option would be to add an order_status segment to your gateway return URL and then to only output the submitted_order_info tag if that segment is present.
I'm fairly sure that clear_cart just removes cart contents rather than flushing CT session data entirely (I think that's only ever triggered by logging out).
I sometimes find this helps clear cart, it works in similar way to {redirect="blah/blah"}
{exp:cartthrob:clear_cart return="about/stuff" }
And of course segment can help if need to trigger via a link
{if segment_3 == "foo"}
{exp:cartthrob:clear_cart return="about/stuff" }
{/if}

Resources