AfRApay.Web: Make transactions idempotent
First rule of networking is the network is unreliable. Sometimes things get lost, sometimes it gets found multiple times for TCP reasons or because a browser tries to be clever. And when you're dealing with money, even if it's monopoly money, you don't want a duplicated request to mean a double-debit. The easiest way to do this is to simply include an idempotency key with each request - if that key is repeated, the request is ignored.
This commit is contained in:
parent
597158a038
commit
0bf9843c1f
|
@ -147,7 +147,10 @@ static async void HandleTap(IsoReader reader, HttpClient httpClient, TerminalSta
|
||||||
case TerminalState.Debit:
|
case TerminalState.Debit:
|
||||||
case TerminalState.Credit:
|
case TerminalState.Credit:
|
||||||
var finalAmount = Math.Abs(amount) * (state == TerminalState.Debit ? 1 : -1);
|
var finalAmount = Math.Abs(amount) * (state == TerminalState.Debit ? 1 : -1);
|
||||||
await CallGet(httpClient, String.Format("/api/card/transaction?card={0}&amount={1}", Convert.ToHexString(uid), finalAmount));
|
var idempotencyKey = new byte[18];
|
||||||
|
Random.Shared.NextBytes(idempotencyKey);
|
||||||
|
await CallGet(httpClient, String.Format("/api/card/transaction?card={0}&amount={1}&ik={2}",
|
||||||
|
Convert.ToHexString(uid), finalAmount, Convert.ToBase64String(idempotencyKey)));
|
||||||
break;
|
break;
|
||||||
case TerminalState.Link:
|
case TerminalState.Link:
|
||||||
await CallGet(httpClient, String.Format("/api/card/link?card={0}", Convert.ToHexString(uid)));
|
await CallGet(httpClient, String.Format("/api/card/link?card={0}", Convert.ToHexString(uid)));
|
||||||
|
|
|
@ -9,4 +9,5 @@ public class User {
|
||||||
[Column(Name = "ID"), PrimaryKey, Identity, NotNull] public int Id { get; set; }
|
[Column(Name = "ID"), PrimaryKey, Identity, NotNull] public int Id { get; set; }
|
||||||
[Column(Name = "Nickname"), NotNull] public string Nickname { get; set; }
|
[Column(Name = "Nickname"), NotNull] public string Nickname { get; set; }
|
||||||
[Column(Name = "Balance")] public decimal Balance { get; set; }
|
[Column(Name = "Balance")] public decimal Balance { get; set; }
|
||||||
|
[Column(Name = "LastIdempotencyKey")] public string LastIdempotencyKey { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,23 @@ namespace AfRApay.Web.Controllers;
|
||||||
[ApiController, Route("/api/card/transaction")]
|
[ApiController, Route("/api/card/transaction")]
|
||||||
public class CardTransaction : Controller {
|
public class CardTransaction : Controller {
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public string Get([FromQuery] string card, [FromQuery] decimal amount) {
|
public string Get([FromQuery] string card, [FromQuery] decimal amount, [FromQuery] string ik) {
|
||||||
var db = new Database.DbConn();
|
var db = new Database.DbConn();
|
||||||
if (db.Cards.Any(p => p.CardId == card)) {
|
if (db.Cards.Any(p => p.CardId == card)) {
|
||||||
var userId = db.Cards.First(p => p.CardId == card).UserId;
|
var userId = db.Cards.First(p => p.CardId == card).UserId;
|
||||||
var user = db.Users.First(p => p.Id == userId);
|
var user = db.Users.First(p => p.Id == userId);
|
||||||
|
|
||||||
|
// If an idempotency key is given, disregard duplicate requests.
|
||||||
|
if (ik == "" || ik != user.LastIdempotencyKey) {
|
||||||
if (user.Balance - amount < -50) {
|
if (user.Balance - amount < -50) {
|
||||||
return "E:Balance too low!";
|
return "E:Balance too low!";
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Balance -= amount;
|
user.Balance -= amount;
|
||||||
|
user.LastIdempotencyKey = ik;
|
||||||
db.Update(user);
|
db.Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
return $"S:{user.Nickname}: {user.Balance:N2}";
|
return $"S:{user.Nickname}: {user.Balance:N2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue