diff --git a/QRBee.Core/Client/Client.cs b/QRBee.Core/Client/Client.cs
index 75ca88b..a9a58fe 100644
--- a/QRBee.Core/Client/Client.cs
+++ b/QRBee.Core/Client/Client.cs
@@ -53,6 +53,142 @@ namespace QRBee.Core.Client
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
+ /// Success
+ /// A server side error occurred.
+ public virtual System.Threading.Tasks.Task AnonymousAsync()
+ {
+ return AnonymousAsync(System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public virtual async System.Threading.Tasks.Task AnonymousAsync(System.Threading.CancellationToken cancellationToken)
+ {
+ var urlBuilder_ = new System.Text.StringBuilder();
+ urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/");
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ request_.Method = new System.Net.Http.HttpMethod("GET");
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ return;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
+ /// Success
+ /// A server side error occurred.
+ public virtual System.Threading.Tasks.Task QRBeeAsync()
+ {
+ return QRBeeAsync(System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public virtual async System.Threading.Tasks.Task QRBeeAsync(System.Threading.CancellationToken cancellationToken)
+ {
+ var urlBuilder_ = new System.Text.StringBuilder();
+ urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/QRBee");
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ request_.Method = new System.Net.Http.HttpMethod("GET");
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ return;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
/// Success
/// A server side error occurred.
public virtual System.Threading.Tasks.Task RegisterAsync(RegistrationRequest body)
@@ -282,6 +418,77 @@ namespace QRBee.Core.Client
}
}
+ /// Success
+ /// A server side error occurred.
+ public virtual System.Threading.Tasks.Task ConfirmPayAsync(PaymentConfirmation body)
+ {
+ return ConfirmPayAsync(body, System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public virtual async System.Threading.Tasks.Task ConfirmPayAsync(PaymentConfirmation body, System.Threading.CancellationToken cancellationToken)
+ {
+ var urlBuilder_ = new System.Text.StringBuilder();
+ urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/QRBee/ConfirmPay");
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value));
+ content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
+ request_.Content = content_;
+ request_.Method = new System.Net.Http.HttpMethod("POST");
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ return;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
protected struct ObjectResponseResult
{
public ObjectResponseResult(T responseObject, string responseText)
diff --git a/QRBee.Core/Data/PaymentConfirmation.cs b/QRBee.Core/Data/PaymentConfirmation.cs
new file mode 100644
index 0000000..3660a8c
--- /dev/null
+++ b/QRBee.Core/Data/PaymentConfirmation.cs
@@ -0,0 +1,11 @@
+namespace QRBee.Core.Data
+{
+ public class PaymentConfirmation
+ {
+ public string MerchantId { get; set; }
+
+ public string MerchantTransactionId { get; set; }
+
+ public string GatewayTransactionId { get; set; }
+ }
+}
diff --git a/QRBee.Core/Data/PaymentResponse.cs b/QRBee.Core/Data/PaymentResponse.cs
index dae046a..7ab7e23 100644
--- a/QRBee.Core/Data/PaymentResponse.cs
+++ b/QRBee.Core/Data/PaymentResponse.cs
@@ -4,6 +4,8 @@
{
public string ServerTransactionId { get; set; }
+ public string GatewayTransactionId { get; set; }
+
public PaymentRequest PaymentRequest { get; set; }
public DateTime ServerTimeStampUTC { get; set; }
@@ -18,6 +20,6 @@
/// Convert PaymentResponse to string to be encrypted and transmitted back to merchant
///
/// Converted string
- public string AsDataForSignature() => $"{ServerTransactionId}|{PaymentRequest.AsString()}|{ServerTimeStampUTC:yyyy-MM-dd:HH.mm.ss.ffff}|{Success}|{RejectReason}";
+ public string AsDataForSignature() => $"{ServerTransactionId}|{GatewayTransactionId}|{PaymentRequest.AsString()}|{ServerTimeStampUTC:yyyy-MM-dd:HH.mm.ss.ffff}|{Success}|{RejectReason}";
}
}
diff --git a/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs b/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs
index 93729ee..5f09db9 100644
--- a/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs
+++ b/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs
@@ -80,6 +80,14 @@ namespace QRBee.ViewModels
if (check)
{
+ var paymentConfirmation = new PaymentConfirmation
+ {
+ MerchantId = _settings.LoadSettings().ClientId,
+ MerchantTransactionId = response.PaymentRequest.ClientResponse.MerchantRequest.MerchantTransactionId,
+ GatewayTransactionId = response.GatewayTransactionId
+ };
+
+ await apiService.ConfirmPayAsync(paymentConfirmation);
await Application.Current.MainPage.DisplayAlert("Success", "The transaction completed successfully ", "Ok");
}
else
diff --git a/QRBeeApi/Controllers/QRBeeController.cs b/QRBeeApi/Controllers/QRBeeController.cs
index deb6441..fe36f0c 100644
--- a/QRBeeApi/Controllers/QRBeeController.cs
+++ b/QRBeeApi/Controllers/QRBeeController.cs
@@ -46,5 +46,12 @@ namespace QRBee.Api.Controllers
_logger.LogInformation($"Trying to insert new transaction {value.ClientResponse.MerchantRequest.MerchantTransactionId}");
return _service.Pay(value);
}
+
+ [HttpPost("ConfirmPay")]
+ public Task ConfirmPay([FromBody] PaymentConfirmation value)
+ {
+ _logger.LogInformation($"Trying to confirm transaction with gatewayTransactionId: {value.GatewayTransactionId}");
+ return _service.ConfirmPay(value);
+ }
}
}
diff --git a/QRBeeApi/Program.cs b/QRBeeApi/Program.cs
index 982096f..52cfba9 100644
--- a/QRBeeApi/Program.cs
+++ b/QRBeeApi/Program.cs
@@ -32,6 +32,7 @@ builder.Services
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
;
var app = builder.Build();
diff --git a/QRBeeApi/Services/Database/IStorage.cs b/QRBeeApi/Services/Database/IStorage.cs
index 2ad371a..a5f9919 100644
--- a/QRBeeApi/Services/Database/IStorage.cs
+++ b/QRBeeApi/Services/Database/IStorage.cs
@@ -44,6 +44,8 @@
/// Transaction information
Task GetTransactionInfoByTransactionId(string id);
+ Task> GetTransactionsByStatus(TransactionInfo.TransactionStatus status);
+
///
/// Update transaction after execution
///
@@ -70,5 +72,7 @@
/// Identifier by which certificate information will be retrieved
/// Certificate information
Task GetCertificateInfoByUserId(string clientId);
+
+
}
}
diff --git a/QRBeeApi/Services/Database/Storage.cs b/QRBeeApi/Services/Database/Storage.cs
index 3feae8d..1c5a16f 100644
--- a/QRBeeApi/Services/Database/Storage.cs
+++ b/QRBeeApi/Services/Database/Storage.cs
@@ -101,6 +101,20 @@ namespace QRBee.Api.Services.Database
return transaction ?? throw new ApplicationException($"Transaction with Id: {id} not found.");
}
+ public async Task> GetTransactionsByStatus(TransactionInfo.TransactionStatus status)
+ {
+ var collection = _database.GetCollection("Transactions");
+ using var cursor = await collection.FindAsync($"{{ Status: {(int)status} }}");
+
+ var result = new List();
+
+ while (await cursor.MoveNextAsync())
+ {
+ result.AddRange(cursor.Current);
+ }
+ return result;
+ }
+
public async Task UpdateTransaction(TransactionInfo info)
{
var collection = _database.GetCollection("Transactions");
diff --git a/QRBeeApi/Services/Database/TransactionInfo.cs b/QRBeeApi/Services/Database/TransactionInfo.cs
index 99f46bb..d682478 100644
--- a/QRBeeApi/Services/Database/TransactionInfo.cs
+++ b/QRBeeApi/Services/Database/TransactionInfo.cs
@@ -29,6 +29,9 @@ namespace QRBee.Api.Services.Database
[BsonId] public string Id { get; set; }
[BsonIgnore] public string? TransactionId => Id;
+
+ public string? GatewayTransactionId { get; set; }
+ public string MerchantTransactionId => Request.ClientResponse.MerchantRequest.MerchantTransactionId;
public DateTime ServerTimeStamp { get; set; }
public PaymentRequest Request { get; set; }
@@ -38,6 +41,9 @@ namespace QRBee.Api.Services.Database
Pending = 0,
Rejected = 1,
Succeeded = 2,
+ Confirmed = 3,
+ Cancelled = 4,
+ CancelFailed =5
}
public TransactionStatus Status { get; set; } = TransactionStatus.Pending;
diff --git a/QRBeeApi/Services/IPaymentGateway.cs b/QRBeeApi/Services/IPaymentGateway.cs
index 9603997..1b28092 100644
--- a/QRBeeApi/Services/IPaymentGateway.cs
+++ b/QRBeeApi/Services/IPaymentGateway.cs
@@ -8,10 +8,14 @@ namespace QRBee.Api.Services
{
public bool Success { get; init; }
public string? ErrorMessage { get; init; }
+
+ public string? GatewayTransactionId { get; init; }
}
public interface IPaymentGateway
{
Task Payment(TransactionInfo info, ClientCardData clientCardData);
+
+ Task CancelPayment(TransactionInfo info);
}
}
diff --git a/QRBeeApi/Services/IQRBeeAPI.cs b/QRBeeApi/Services/IQRBeeAPI.cs
index ada8c4a..e8d4b84 100644
--- a/QRBeeApi/Services/IQRBeeAPI.cs
+++ b/QRBeeApi/Services/IQRBeeAPI.cs
@@ -27,5 +27,7 @@ namespace QRBee.Api.Services
/// Payment request
Task Pay(PaymentRequest value);
+ Task ConfirmPay(PaymentConfirmation value);
+
}
}
diff --git a/QRBeeApi/Services/PaymentGateway.cs b/QRBeeApi/Services/PaymentGateway.cs
index 0d48c74..e04e88e 100644
--- a/QRBeeApi/Services/PaymentGateway.cs
+++ b/QRBeeApi/Services/PaymentGateway.cs
@@ -20,13 +20,36 @@ internal class PaymentGateway : IPaymentGateway
return Task.FromResult(new GatewayResponse
{
Success = false,
- ErrorMessage = "Amount is too low"
+ ErrorMessage = "Amount is too low",
+ GatewayTransactionId = Guid.NewGuid().ToString()
});
}
_logger.LogInformation($"Transaction with id: {info.Id} succeeded");
return Task.FromResult(new GatewayResponse
{
- Success = true
+ Success = true,
+ GatewayTransactionId = Guid.NewGuid().ToString()
+ });
+ }
+
+ public Task CancelPayment(TransactionInfo info)
+ {
+ if (!string.IsNullOrWhiteSpace(info.GatewayTransactionId))
+ {
+ _logger.LogInformation($"Transaction with id: {info.Id} was cancelled");
+ return Task.FromResult(new GatewayResponse
+ {
+ Success = false,
+ ErrorMessage = "Either payment gateway isn't working or the transaction is old",
+ GatewayTransactionId = info.GatewayTransactionId
+ });
+ }
+
+ _logger.LogInformation($"Transaction with id: {info.Id} succeeded");
+ return Task.FromResult(new GatewayResponse
+ {
+ Success = true,
+ GatewayTransactionId = info.GatewayTransactionId
});
}
}
\ No newline at end of file
diff --git a/QRBeeApi/Services/QRBeeAPIService.cs b/QRBeeApi/Services/QRBeeAPIService.cs
index de25b57..43f6d71 100644
--- a/QRBeeApi/Services/QRBeeAPIService.cs
+++ b/QRBeeApi/Services/QRBeeAPIService.cs
@@ -19,18 +19,20 @@ namespace QRBee.Api.Services
private readonly IPrivateKeyHandler _privateKeyHandler;
private readonly IPaymentGateway _paymentGateway;
private readonly ILogger _logger;
+ private readonly TransactionMonitoring _transactionMonitoring;
private static readonly object _lock = new ();
private const int MaxNameLength = 512;
private const int MaxEmailLength = 512;
- public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler, IPaymentGateway paymentGateway, ILogger logger)
+ public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler, IPaymentGateway paymentGateway, ILogger logger, TransactionMonitoring transactionMonitoring)
{
_storage = storage;
_securityService = securityService;
_privateKeyHandler = privateKeyHandler;
_paymentGateway = paymentGateway;
_logger = logger;
+ _transactionMonitoring = transactionMonitoring;
Init(_privateKeyHandler);
}
@@ -180,39 +182,41 @@ namespace QRBee.Api.Services
_logger.LogInformation($"Transaction=\"{tid}\" initialized");
//8. Send client card data to a payment gateway
- var res = await _paymentGateway.Payment(info, clientCardData);
+ var gatewayResponse = await _paymentGateway.Payment(info, clientCardData);
//9. Record transaction with result
- if (res.Success)
+ if (gatewayResponse.Success)
{
info.Status=TransactionInfo.TransactionStatus.Succeeded;
+ info.GatewayTransactionId=gatewayResponse.GatewayTransactionId;
}
else
{
info.Status = TransactionInfo.TransactionStatus.Rejected;
- info.RejectReason = res.ErrorMessage;
+ info.RejectReason = gatewayResponse.ErrorMessage;
}
await _storage.UpdateTransaction(info);
_logger.LogInformation($"Transaction=\"{tid}\" complete Status=\"{info.Status}\"");
//10. Make response for merchant
- var response = MakePaymentResponse(value, info.TransactionId ?? "", info.Status==TransactionInfo.TransactionStatus.Succeeded, info.RejectReason);
+ var response = MakePaymentResponse(value, info.TransactionId ?? "", gatewayResponse.GatewayTransactionId ?? "", info.Status==TransactionInfo.TransactionStatus.Succeeded, info.RejectReason);
return response;
}
catch (Exception e)
{
- var response = MakePaymentResponse(value, "", false, e.Message);
+ var response = MakePaymentResponse(value, "", "", false, e.Message);
return response;
}
}
- private PaymentResponse MakePaymentResponse(PaymentRequest value, string transactionId, bool result = true, string? errorMessage = null)
+ private PaymentResponse MakePaymentResponse(PaymentRequest value, string transactionId, string gatewayTransactionId, bool result = true, string? errorMessage = null)
{
var response = new PaymentResponse
{
ServerTransactionId = transactionId,
+ GatewayTransactionId = gatewayTransactionId,
PaymentRequest = value,
ServerTimeStampUTC = DateTime.UtcNow,
Success = result,
@@ -364,5 +368,21 @@ namespace QRBee.Api.Services
};
}
+ public async Task ConfirmPay(PaymentConfirmation value)
+ {
+ var id = $"{value.MerchantId}-{value.MerchantTransactionId}";
+ var trans = await _storage.GetTransactionInfoByTransactionId(id);
+ if (trans.GatewayTransactionId == value.GatewayTransactionId)
+ {
+ trans.Status = TransactionInfo.TransactionStatus.Confirmed;
+ await _storage.UpdateTransaction(trans);
+ _logger.LogInformation($"Transaction with MerchantTransactionId: {trans.MerchantTransactionId} confirmed");
+ }
+ else
+ {
+ throw new ApplicationException($"Transaction with gatewayTransactionId:{value.GatewayTransactionId} failed.");
+ }
+ }
+
}
}
diff --git a/QRBeeApi/Services/TransactionMonitoring.cs b/QRBeeApi/Services/TransactionMonitoring.cs
new file mode 100644
index 0000000..93e4cc0
--- /dev/null
+++ b/QRBeeApi/Services/TransactionMonitoring.cs
@@ -0,0 +1,80 @@
+using QRBee.Api.Services.Database;
+
+namespace QRBee.Api.Services
+{
+ public class TransactionMonitoring
+ {
+ private readonly IStorage _storage;
+ private readonly IPaymentGateway _paymentGateway;
+ private readonly ILogger _logger;
+ private const double Minutes = 5;
+
+ private static bool _started;
+ private static object _syncObject = new();
+
+ public TransactionMonitoring(IStorage storage, IPaymentGateway paymentGateway, ILogger logger)
+ {
+ _storage = storage;
+ _paymentGateway = paymentGateway;
+ _logger = logger;
+
+ if (_started)
+ return;
+
+ lock (_syncObject)
+ {
+ if (_started)
+ return;
+
+ Task.Run(MonitoringLoop);
+ _started = true;
+ }
+
+ }
+ private async Task MonitoringLoop()
+ {
+ _logger.LogInformation("Starting monitoring loop");
+ while (true)
+ {
+ await CheckTransactions();
+ await Task.Delay(TimeSpan.FromMinutes(Minutes));
+ }
+ }
+
+ private async Task CheckTransactions()
+ {
+ var list = await _storage.GetTransactionsByStatus(TransactionInfo.TransactionStatus.Succeeded);
+ _logger.LogDebug($"Found {list.Count} unconfirmed transactions");
+
+ foreach (var transaction in list)
+ {
+ if (transaction.ServerTimeStamp + TimeSpan.FromMinutes(Minutes) > DateTime.UtcNow)
+ {
+ _logger.LogDebug($"Transaction: {transaction.MerchantTransactionId} should not be cancelled yet (ServerTimeStamp: {transaction.ServerTimeStamp:O}, Now: {DateTime.UtcNow:O})");
+ continue;
+ }
+
+ _logger.LogDebug($"Cancelling transaction: {transaction.MerchantTransactionId}...");
+ await CancelTransaction(transaction);
+ }
+ }
+
+ private async Task CancelTransaction(TransactionInfo transaction)
+ {
+ try
+ {
+ await _paymentGateway.CancelPayment(transaction);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, $"Transaction: {transaction.MerchantTransactionId} can't be cancelled: {e.Message}");
+ transaction.Status = TransactionInfo.TransactionStatus.CancelFailed;
+ await _storage.UpdateTransaction(transaction);
+ return;
+ }
+
+ transaction.Status = TransactionInfo.TransactionStatus.Cancelled;
+ await _storage.UpdateTransaction(transaction);
+ }
+ }
+}