Transaction monitoring implemented. Merchant confirmation implemented.

This commit is contained in:
Andrey Shabarshov 2022-04-12 12:50:05 +01:00
parent 400a41760b
commit 5597fff1ab
14 changed files with 399 additions and 10 deletions

View File

@ -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 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); partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual System.Threading.Tasks.Task AnonymousAsync()
{
return AnonymousAsync(System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
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();
}
}
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual System.Threading.Tasks.Task QRBeeAsync()
{
return QRBeeAsync(System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
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();
}
}
/// <returns>Success</returns> /// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception> /// <exception cref="ApiException">A server side error occurred.</exception>
public virtual System.Threading.Tasks.Task<RegistrationResponse> RegisterAsync(RegistrationRequest body) public virtual System.Threading.Tasks.Task<RegistrationResponse> RegisterAsync(RegistrationRequest body)
@ -282,6 +418,77 @@ namespace QRBee.Core.Client
} }
} }
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual System.Threading.Tasks.Task ConfirmPayAsync(PaymentConfirmation body)
{
return ConfirmPayAsync(body, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
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<T> protected struct ObjectResponseResult<T>
{ {
public ObjectResponseResult(T responseObject, string responseText) public ObjectResponseResult(T responseObject, string responseText)

View File

@ -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; }
}
}

View File

@ -4,6 +4,8 @@
{ {
public string ServerTransactionId { get; set; } public string ServerTransactionId { get; set; }
public string GatewayTransactionId { get; set; }
public PaymentRequest PaymentRequest { get; set; } public PaymentRequest PaymentRequest { get; set; }
public DateTime ServerTimeStampUTC { get; set; } public DateTime ServerTimeStampUTC { get; set; }
@ -18,6 +20,6 @@
/// Convert PaymentResponse to string to be encrypted and transmitted back to merchant /// Convert PaymentResponse to string to be encrypted and transmitted back to merchant
/// </summary> /// </summary>
/// <returns>Converted string</returns> /// <returns>Converted string</returns>
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}";
} }
} }

View File

@ -80,6 +80,14 @@ namespace QRBee.ViewModels
if (check) 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"); await Application.Current.MainPage.DisplayAlert("Success", "The transaction completed successfully ", "Ok");
} }
else else

View File

@ -46,5 +46,12 @@ namespace QRBee.Api.Controllers
_logger.LogInformation($"Trying to insert new transaction {value.ClientResponse.MerchantRequest.MerchantTransactionId}"); _logger.LogInformation($"Trying to insert new transaction {value.ClientResponse.MerchantRequest.MerchantTransactionId}");
return _service.Pay(value); 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);
}
} }
} }

View File

@ -32,6 +32,7 @@ builder.Services
.AddSingleton<IPrivateKeyHandler, ServerPrivateKeyHandler>() .AddSingleton<IPrivateKeyHandler, ServerPrivateKeyHandler>()
.AddSingleton<ISecurityService, SecurityService>() .AddSingleton<ISecurityService, SecurityService>()
.AddSingleton<IPaymentGateway, PaymentGateway>() .AddSingleton<IPaymentGateway, PaymentGateway>()
.AddSingleton<TransactionMonitoring>()
; ;
var app = builder.Build(); var app = builder.Build();

View File

@ -44,6 +44,8 @@
/// <returns>Transaction information</returns> /// <returns>Transaction information</returns>
Task<TransactionInfo> GetTransactionInfoByTransactionId(string id); Task<TransactionInfo> GetTransactionInfoByTransactionId(string id);
Task<List<TransactionInfo>> GetTransactionsByStatus(TransactionInfo.TransactionStatus status);
/// <summary> /// <summary>
/// Update transaction after execution /// Update transaction after execution
/// </summary> /// </summary>
@ -70,5 +72,7 @@
/// <param name="clientId">Identifier by which certificate information will be retrieved</param> /// <param name="clientId">Identifier by which certificate information will be retrieved</param>
/// <returns>Certificate information</returns> /// <returns>Certificate information</returns>
Task<CertificateInfo> GetCertificateInfoByUserId(string clientId); Task<CertificateInfo> GetCertificateInfoByUserId(string clientId);
} }
} }

View File

@ -101,6 +101,20 @@ namespace QRBee.Api.Services.Database
return transaction ?? throw new ApplicationException($"Transaction with Id: {id} not found."); return transaction ?? throw new ApplicationException($"Transaction with Id: {id} not found.");
} }
public async Task<List<TransactionInfo>> GetTransactionsByStatus(TransactionInfo.TransactionStatus status)
{
var collection = _database.GetCollection<TransactionInfo>("Transactions");
using var cursor = await collection.FindAsync($"{{ Status: {(int)status} }}");
var result = new List<TransactionInfo>();
while (await cursor.MoveNextAsync())
{
result.AddRange(cursor.Current);
}
return result;
}
public async Task UpdateTransaction(TransactionInfo info) public async Task UpdateTransaction(TransactionInfo info)
{ {
var collection = _database.GetCollection<TransactionInfo>("Transactions"); var collection = _database.GetCollection<TransactionInfo>("Transactions");

View File

@ -29,6 +29,9 @@ namespace QRBee.Api.Services.Database
[BsonId] public string Id { get; set; } [BsonId] public string Id { get; set; }
[BsonIgnore] public string? TransactionId => Id; [BsonIgnore] public string? TransactionId => Id;
public string? GatewayTransactionId { get; set; }
public string MerchantTransactionId => Request.ClientResponse.MerchantRequest.MerchantTransactionId;
public DateTime ServerTimeStamp { get; set; } public DateTime ServerTimeStamp { get; set; }
public PaymentRequest Request { get; set; } public PaymentRequest Request { get; set; }
@ -38,6 +41,9 @@ namespace QRBee.Api.Services.Database
Pending = 0, Pending = 0,
Rejected = 1, Rejected = 1,
Succeeded = 2, Succeeded = 2,
Confirmed = 3,
Cancelled = 4,
CancelFailed =5
} }
public TransactionStatus Status { get; set; } = TransactionStatus.Pending; public TransactionStatus Status { get; set; } = TransactionStatus.Pending;

View File

@ -8,10 +8,14 @@ namespace QRBee.Api.Services
{ {
public bool Success { get; init; } public bool Success { get; init; }
public string? ErrorMessage { get; init; } public string? ErrorMessage { get; init; }
public string? GatewayTransactionId { get; init; }
} }
public interface IPaymentGateway public interface IPaymentGateway
{ {
Task<GatewayResponse> Payment(TransactionInfo info, ClientCardData clientCardData); Task<GatewayResponse> Payment(TransactionInfo info, ClientCardData clientCardData);
Task<GatewayResponse> CancelPayment(TransactionInfo info);
} }
} }

View File

@ -27,5 +27,7 @@ namespace QRBee.Api.Services
/// <param name="value">Payment request</param> /// <param name="value">Payment request</param>
Task<PaymentResponse> Pay(PaymentRequest value); Task<PaymentResponse> Pay(PaymentRequest value);
Task ConfirmPay(PaymentConfirmation value);
} }
} }

View File

@ -20,13 +20,36 @@ internal class PaymentGateway : IPaymentGateway
return Task.FromResult(new GatewayResponse return Task.FromResult(new GatewayResponse
{ {
Success = false, Success = false,
ErrorMessage = "Amount is too low" ErrorMessage = "Amount is too low",
GatewayTransactionId = Guid.NewGuid().ToString()
}); });
} }
_logger.LogInformation($"Transaction with id: {info.Id} succeeded"); _logger.LogInformation($"Transaction with id: {info.Id} succeeded");
return Task.FromResult(new GatewayResponse return Task.FromResult(new GatewayResponse
{ {
Success = true Success = true,
GatewayTransactionId = Guid.NewGuid().ToString()
});
}
public Task<GatewayResponse> 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
}); });
} }
} }

View File

@ -19,18 +19,20 @@ namespace QRBee.Api.Services
private readonly IPrivateKeyHandler _privateKeyHandler; private readonly IPrivateKeyHandler _privateKeyHandler;
private readonly IPaymentGateway _paymentGateway; private readonly IPaymentGateway _paymentGateway;
private readonly ILogger<QRBeeAPIService> _logger; private readonly ILogger<QRBeeAPIService> _logger;
private readonly TransactionMonitoring _transactionMonitoring;
private static readonly object _lock = new (); private static readonly object _lock = new ();
private const int MaxNameLength = 512; private const int MaxNameLength = 512;
private const int MaxEmailLength = 512; private const int MaxEmailLength = 512;
public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler, IPaymentGateway paymentGateway, ILogger<QRBeeAPIService> logger) public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler, IPaymentGateway paymentGateway, ILogger<QRBeeAPIService> logger, TransactionMonitoring transactionMonitoring)
{ {
_storage = storage; _storage = storage;
_securityService = securityService; _securityService = securityService;
_privateKeyHandler = privateKeyHandler; _privateKeyHandler = privateKeyHandler;
_paymentGateway = paymentGateway; _paymentGateway = paymentGateway;
_logger = logger; _logger = logger;
_transactionMonitoring = transactionMonitoring;
Init(_privateKeyHandler); Init(_privateKeyHandler);
} }
@ -180,39 +182,41 @@ namespace QRBee.Api.Services
_logger.LogInformation($"Transaction=\"{tid}\" initialized"); _logger.LogInformation($"Transaction=\"{tid}\" initialized");
//8. Send client card data to a payment gateway //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 //9. Record transaction with result
if (res.Success) if (gatewayResponse.Success)
{ {
info.Status=TransactionInfo.TransactionStatus.Succeeded; info.Status=TransactionInfo.TransactionStatus.Succeeded;
info.GatewayTransactionId=gatewayResponse.GatewayTransactionId;
} }
else else
{ {
info.Status = TransactionInfo.TransactionStatus.Rejected; info.Status = TransactionInfo.TransactionStatus.Rejected;
info.RejectReason = res.ErrorMessage; info.RejectReason = gatewayResponse.ErrorMessage;
} }
await _storage.UpdateTransaction(info); await _storage.UpdateTransaction(info);
_logger.LogInformation($"Transaction=\"{tid}\" complete Status=\"{info.Status}\""); _logger.LogInformation($"Transaction=\"{tid}\" complete Status=\"{info.Status}\"");
//10. Make response for merchant //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; return response;
} }
catch (Exception e) catch (Exception e)
{ {
var response = MakePaymentResponse(value, "", false, e.Message); var response = MakePaymentResponse(value, "", "", false, e.Message);
return response; 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 var response = new PaymentResponse
{ {
ServerTransactionId = transactionId, ServerTransactionId = transactionId,
GatewayTransactionId = gatewayTransactionId,
PaymentRequest = value, PaymentRequest = value,
ServerTimeStampUTC = DateTime.UtcNow, ServerTimeStampUTC = DateTime.UtcNow,
Success = result, 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.");
}
}
} }
} }

View File

@ -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<TransactionMonitoring> _logger;
private const double Minutes = 5;
private static bool _started;
private static object _syncObject = new();
public TransactionMonitoring(IStorage storage, IPaymentGateway paymentGateway, ILogger<TransactionMonitoring> 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);
}
}
}