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); + } + } +}