Let say this is my API:
app.post('/refund', function (req, res) {
Transaction.findOneAndUpdate({_id: req.body.transaction_id}, {$set: {refund_status: true}}).
exec(function (err, transaction_status) {
res.end("Refund successfully")
}
}
If two admins click the refund button at the same time. My customer will receive a double refund.
So, how to prevent these issues?
CodePudding user response:
Simply get the transaction first and check the refund status and update. If its false refund. If its true tell its already refunded.
app.post('/refund', function (req, res) {
// get the transaction first
Transaction.find({_id: req.body.transaction_id}, function(err, doc) {
if (doc.refund_status == false) {
Transaction.findOneAndUpdate({_id: req.body.transaction_id}, {$set: {refund_status: true}}).exec(function(err, transaction_status) {
res.end("Refund successfully")
})
} else { // already refunded
res.end("Already refunded")
}
})
}
CodePudding user response:
Firstly, You should disable the availability to click the refund button twice before the first is finished (for a better UX).
and to prevent that from happening on the backend, you can use rate limit the specific route.
if you're using express, you should look into express-slow-down or express-rate-limit
A possible solution will be:
const slowDown = require("express-slow-down");
/* app.enable("trust proxy"); only if you're behind a reverse proxy (Heroku,
Bluemix, AWS if you use an ELB, custom Nginx setup, etc) */
const refundSpeedLimiter = slowDown({
windowMs: 10 * 1000, // 10 seconds
delayAfter: 1, // allow the first request to go at full-speed, then...
delayMs: 1.5 * 1000 // 2nd request has a **1.5s** delay, 3rd has a **3s** delay, 4th gets 4.5s, etc.
});
app.post('/refund', refundSpeedLimiter, function (req, res) {
// Fetch The Transaction
Transaction.find({_id: req.body.transaction_id}, function(err, doc) {
// Check if its not already refunded.
if (doc.refund_status == false) {
Transaction.findOneAndUpdate({_id: req.body.transaction_id}, {$set: {refund_status: true}}).exec(function(err, transaction_status) {
res.end("Refund successfully")
})
} else { // already refunded
res.end("Already refunded")
}
})
}
as mentioned in express-rate-limit
:
This module does not share state with other processes/servers by default. If you need a more robust solution, I recommend using an external store. See the stores section below for a list of external stores.
Notice that the default store is the Memory store, so it allows multiple instances by default. if you intend to use a different store (which allows the use of rate limiting across multiple node instances - like in cluster mode), you will need to set a custom key for each ratelimit
instance.
CodePudding user response:
Implement your refund logic in a callback from session.withTransaction(). That will prevent concurrent updates. Don't worry about the async stuff; .withTransaction() handles it correctly.
Or, make your refund logic idempotent. That is, implement it in a way that calling it multiple times does exactly the same thing as calling it once. Your setup is almost idempotent as it is: you start with the transaction id and set its refund statuse to true. But the idempotence may not work for you if you must report when the transaction is already refunded.