Work on Transaction started. Pre-checks complete.

This commit is contained in:
Andrey Shabarshov 2022-03-04 17:17:10 +00:00
parent 4722585e17
commit 82b8d1820e
10 changed files with 198 additions and 36 deletions

View File

@ -207,18 +207,18 @@ namespace QRBee.Core.Client
/// <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 InsertTransactionAsync(PaymentRequest body) public virtual System.Threading.Tasks.Task<PaymentResponse> PayAsync(PaymentRequest body)
{ {
return InsertTransactionAsync(body, System.Threading.CancellationToken.None); return PayAsync(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> /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <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 async System.Threading.Tasks.Task InsertTransactionAsync(PaymentRequest body, System.Threading.CancellationToken cancellationToken) public virtual async System.Threading.Tasks.Task<PaymentResponse> PayAsync(PaymentRequest body, System.Threading.CancellationToken cancellationToken)
{ {
var urlBuilder_ = new System.Text.StringBuilder(); var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/QRBee/InsertTransaction"); urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/QRBee/Pay");
var client_ = _httpClient; var client_ = _httpClient;
var disposeClient_ = false; var disposeClient_ = false;
@ -230,6 +230,7 @@ namespace QRBee.Core.Client
content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
request_.Content = content_; request_.Content = content_;
request_.Method = new System.Net.Http.HttpMethod("POST"); request_.Method = new System.Net.Http.HttpMethod("POST");
request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));
PrepareRequest(client_, request_, urlBuilder_); PrepareRequest(client_, request_, urlBuilder_);
@ -254,7 +255,12 @@ namespace QRBee.Core.Client
var status_ = (int)response_.StatusCode; var status_ = (int)response_.StatusCode;
if (status_ == 200) if (status_ == 200)
{ {
return; var objectResponse_ = await ReadObjectResponseAsync<PaymentResponse>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
return objectResponse_.Object;
} }
else else
{ {

View File

@ -15,7 +15,36 @@
/// WARNING: this should always be encrypted and never transmitted in clear text form. /// WARNING: this should always be encrypted and never transmitted in clear text form.
/// </summary> /// </summary>
/// <returns>Converted string</returns> /// <returns>Converted string</returns>
public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo}"; public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo ?? 0}";
public static ClientCardData FromString(string input)
{
var s = input.Split('|');
if (s.Length < 7)
{
throw new ApplicationException("Expected 7 or more elements");
}
var res = new ClientCardData()
{
TransactionId = s[0],
CardNumber = s[1],
ExpirationDateMMYY = s[2],
ValidFrom = s[3],
CardHolderName = s[4],
CVC = s[5]
};
if (!string.IsNullOrWhiteSpace(s[6]))
res.IssueNo = Convert.ToInt32(s[6]);
if (res.IssueNo <= 0)
{
res.IssueNo = null;
}
return res;
}
} }
} }

View File

@ -30,16 +30,10 @@ namespace QRBee.Droid
LoadApplication(new App(AddServices)); LoadApplication(new App(AddServices));
ZXing.Mobile.MobileBarcodeScanner.Initialize(Application); ZXing.Mobile.MobileBarcodeScanner.Initialize(Application);
if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.Camera) == (int) Permission.Granted) if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.Camera) != (int) Permission.Granted)
{ {
ActivityCompat.RequestPermissions(this, new String[] {Manifest.Permission.Camera}, 0);
} }
else
{
ActivityCompat.RequestPermissions(this, new String[] { Manifest.Permission.Camera }, 0);
// ActivityCompat.RequestPermissions(this, new String[] { Manifest.Permission.UseFingerprint }, 0);
}
} }
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)

View File

@ -48,8 +48,9 @@ namespace QRBee.ViewModels
//QrCode = null; //QrCode = null;
IsVisible = false; IsVisible = false;
await service.InsertTransactionAsync(paymentRequest); var response = await service.PayAsync(paymentRequest);
//TODO handle response
await Application.Current.MainPage.DisplayAlert("Success", "The transaction completed successfully ", "Ok"); await Application.Current.MainPage.DisplayAlert("Success", "The transaction completed successfully ", "Ok");
} }
catch (Exception e) catch (Exception e)

View File

@ -35,11 +35,11 @@ namespace QRBee.Api.Controllers
return _service.Update(clientId,value); return _service.Update(clientId,value);
} }
[HttpPost("InsertTransaction")] [HttpPost("Pay")]
public Task InsertTransaction([FromBody] PaymentRequest value) public Task<PaymentResponse> Pay([FromBody] PaymentRequest value)
{ {
_logger.LogInformation($"Trying to insert new transaction {value.ClientResponse.MerchantRequest.MerchantTransactionId}"); _logger.LogInformation($"Trying to insert new transaction {value.ClientResponse.MerchantRequest.MerchantTransactionId}");
return _service.InsertTransaction(value); return _service.Pay(value);
} }
} }
} }

View File

@ -31,6 +31,13 @@
/// <param name="info">Information to be inserted</param> /// <param name="info">Information to be inserted</param>
Task PutTransactionInfo(TransactionInfo info); Task PutTransactionInfo(TransactionInfo info);
/// <summary>
/// Retrieve transaction information from database
/// </summary>
/// <param name="id">Identifier by which transaction information will be retrieved</param>
/// <returns>Transaction information</returns>
Task<TransactionInfo> GetTransactionInfoByTransactionId(string id);
/// <summary> /// <summary>
/// Inserts CertificateInfo into database /// Inserts CertificateInfo into database
/// </summary> /// </summary>
@ -43,6 +50,13 @@
/// </summary> /// </summary>
/// <param name="id">Identifier by which certificate information will be retrieved</param> /// <param name="id">Identifier by which certificate information will be retrieved</param>
/// <returns>Certificate information</returns> /// <returns>Certificate information</returns>
Task<CertificateInfo> GetCertificateInfo(string id); Task<CertificateInfo> GetCertificateInfoByCertificateId(string id);
/// <summary>
/// Retrieve certificate information from database
/// </summary>
/// <param name="clientId">Identifier by which certificate information will be retrieved</param>
/// <returns>Certificate information</returns>
Task<CertificateInfo> GetCertificateInfoByUserId(string clientId);
} }
} }

View File

@ -102,6 +102,12 @@ namespace QRBee.Api.Services.Database
return cursor.Current.FirstOrDefault(); return cursor.Current.FirstOrDefault();
} }
public async Task<TransactionInfo> GetTransactionInfoByTransactionId(string id)
{
var transaction = await TryGetTransactionInfo(id);
return transaction ?? throw new ApplicationException($"Transaction with Id: {id} not found.");
}
public async Task InsertCertificate(CertificateInfo info) public async Task InsertCertificate(CertificateInfo info)
{ {
var collection = _database.GetCollection<CertificateInfo>("Certificates"); var collection = _database.GetCollection<CertificateInfo>("Certificates");
@ -131,7 +137,7 @@ namespace QRBee.Api.Services.Database
/// <returns>null if certificate doesn't exist or CertificateInfo</returns> /// <returns>null if certificate doesn't exist or CertificateInfo</returns>
private async Task<CertificateInfo?> TryGetCertificateInfo(string id) private async Task<CertificateInfo?> TryGetCertificateInfo(string id)
{ {
var collection = _database.GetCollection<CertificateInfo>("Transactions"); var collection = _database.GetCollection<CertificateInfo>("Certificates");
using var cursor = await collection.FindAsync($"{{ Id: \"{id}\" }}"); using var cursor = await collection.FindAsync($"{{ Id: \"{id}\" }}");
if (!await cursor.MoveNextAsync()) if (!await cursor.MoveNextAsync())
{ {
@ -141,10 +147,23 @@ namespace QRBee.Api.Services.Database
return cursor.Current.FirstOrDefault(); return cursor.Current.FirstOrDefault();
} }
public async Task<CertificateInfo> GetCertificateInfo(string id) public async Task<CertificateInfo> GetCertificateInfoByCertificateId(string id)
{ {
var certificate = await TryGetCertificateInfo(id); var certificate = await TryGetCertificateInfo(id);
return certificate ?? throw new ApplicationException($"Certificate with Id: {id} not found."); return certificate ?? throw new ApplicationException($"Certificate with Id: {id} not found.");
} }
public async Task<CertificateInfo> GetCertificateInfoByUserId(string clientId)
{
var collection = _database.GetCollection<CertificateInfo>("Certificates");
using var cursor = await collection.FindAsync($"{{ ClientId: \"{clientId}\" }}");
if (!await cursor.MoveNextAsync())
{
throw new ApplicationException($"Certificate with ClientId: {clientId} not found.");
}
return cursor.Current.FirstOrDefault() ?? throw new ApplicationException($"Certificate with ClientId: {clientId} not found.");
}
} }
} }

View File

@ -34,5 +34,12 @@ namespace QRBee.Api.Services.Database
public PaymentRequest Request { get; set; } public PaymentRequest Request { get; set; }
public enum TransactionStatus
{
Pending = 0,
Rejected = 1,
Succeeded = 2,
}
public TransactionStatus Status { get; set; } = TransactionStatus.Pending;
} }
} }

View File

@ -28,7 +28,7 @@ namespace QRBee.Api.Services
/// Handles InsertTransaction request /// Handles InsertTransaction request
/// </summary> /// </summary>
/// <param name="value">Payment request</param> /// <param name="value">Payment request</param>
Task InsertTransaction(PaymentRequest value); Task<PaymentResponse> Pay(PaymentRequest value);
} }
} }

View File

@ -45,7 +45,7 @@ namespace QRBee.Api.Services
public async Task<RegistrationResponse> Register(RegistrationRequest request) public async Task<RegistrationResponse> Register(RegistrationRequest request)
{ {
Validate(request); ValidateRegistration(request);
var info = Convert(request); var info = Convert(request);
@ -69,18 +69,11 @@ namespace QRBee.Api.Services
public Task Update(string clientId, RegistrationRequest request) public Task Update(string clientId, RegistrationRequest request)
{ {
Validate(request); ValidateRegistration(request);
var info = Convert(request); var info = Convert(request);
return _storage.UpdateUser(info); return _storage.UpdateUser(info);
} }
private void ValidateRegistration(RegistrationRequest request)
public Task InsertTransaction(PaymentRequest value)
{
var info = Convert(value);
return _storage.PutTransactionInfo(info);
}
private void Validate(RegistrationRequest request)
{ {
if (request == null) if (request == null)
{ {
@ -92,14 +85,14 @@ namespace QRBee.Api.Services
var dateOfBirth = request.DateOfBirth; var dateOfBirth = request.DateOfBirth;
var certificateRequest = request.CertificateRequest; var certificateRequest = request.CertificateRequest;
if (string.IsNullOrEmpty(name) || name.All(char.IsLetter)==false || name.Length>=MaxNameLength) if (string.IsNullOrEmpty(name) || name.All(char.IsLetter) == false || name.Length >= MaxNameLength)
{ {
throw new ApplicationException($"Name \"{name}\" isn't valid"); throw new ApplicationException($"Name \"{name}\" isn't valid");
} }
var freq = Regex.Matches(email, @"[^@]+@[^@]+").Count; var freq = Regex.Matches(email, @"[^@]+@[^@]+").Count;
if (string.IsNullOrEmpty(email) || email.IndexOf('@')<0 || freq>=2 || email.Length >= MaxEmailLength) if (string.IsNullOrEmpty(email) || email.IndexOf('@') < 0 || freq >= 2 || email.Length >= MaxEmailLength)
{ {
throw new ApplicationException($"Email \"{email}\" isn't valid"); throw new ApplicationException($"Email \"{email}\" isn't valid");
} }
@ -122,7 +115,7 @@ namespace QRBee.Api.Services
signature, signature,
HashAlgorithmName.SHA256, HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1 RSASignaturePadding.Pkcs1
); );
if (!verified) if (!verified)
{ {
@ -130,6 +123,105 @@ namespace QRBee.Api.Services
} }
} }
public async Task<PaymentResponse> Pay(PaymentRequest value)
{
//1. Check payment request parameters for validity
ValidateTransaction(value);
//2. Check client signature
var t2 = CheckSignature(
value.ClientResponse.AsDataForSignature(),
value.ClientResponse.ClientSignature,
value.ClientResponse.ClientId);
//3. Check merchant signature
var t3 = CheckSignature(
value.ClientResponse.MerchantRequest.AsDataForSignature(),
value.ClientResponse.MerchantRequest.MerchantSignature,
value.ClientResponse.MerchantRequest.MerchantId);
//4. Check if transaction was already processed
var t4 = CheckTransaction(value.ClientResponse.MerchantRequest.MerchantTransactionId);
//Parallel task execution
await Task.WhenAll(t2, t3, t4);
//5. Decrypt client card data
var clientCardData = DecryptClientData(value.ClientResponse.EncryptedClientCardData);
//6. Check client card data for validity
//7. Register preliminary transaction record with expiry of one minute
//8. Send client card data to a payment gateway
//9. Record transaction with result
//10. Make response for merchant
var info = Convert(value);
await _storage.PutTransactionInfo(info);
return new PaymentResponse();
}
private void ValidateTransaction(PaymentRequest request)
{
if (request == null)
{
throw new NullReferenceException();
}
var clientId = request.ClientResponse.ClientId;
var merchantId = request.ClientResponse.MerchantRequest.MerchantId;
var transactionId = request.ClientResponse.MerchantRequest.MerchantTransactionId;
var amount = request.ClientResponse.MerchantRequest.Amount;
if (clientId == null || merchantId == null || transactionId == null)
{
throw new ApplicationException("Id isn't valid");
}
if (amount is <= 0 or >= 10000)
{
throw new ApplicationException($"Amount \"{amount}\" isn't valid");
}
}
private async Task CheckSignature(string data,string signature, string id)
{
var info = await _storage.GetCertificateInfoByUserId(id);
var certificate = _securityService.Deserialize(info.Certificate);
var check = _securityService.Verify(
Encoding.UTF8.GetBytes(data),
System.Convert.FromBase64String(signature),
certificate);
if (!check)
{
throw new ApplicationException($"Signature is incorrect for Id: {id}.");
}
}
private async Task CheckTransaction(string transactionId)
{
var info = await _storage.GetTransactionInfoByTransactionId(transactionId);
switch (info.Status)
{
case TransactionInfo.TransactionStatus.Succeeded:
throw new ApplicationException($"Transaction with Id: {transactionId} was already made.");
case TransactionInfo.TransactionStatus.Rejected:
throw new ApplicationException($"Transaction with Id: {transactionId} is not valid.");
case TransactionInfo.TransactionStatus.Pending:
throw new ApplicationException($"Transaction with Id: {transactionId} is already in progress.");
default:
return;
}
}
private ClientCardData DecryptClientData(string encryptedClientCardData)
{
var info = System.Convert.FromBase64String(encryptedClientCardData);
var bytes = _securityService.Decrypt(info);
var s = Encoding.UTF8.GetString(bytes);
return ClientCardData.FromString(s);
}
private static RSA LoadRsaPublicKey(StringRSAParameters stringParameters) private static RSA LoadRsaPublicKey(StringRSAParameters stringParameters)
{ {
var rsaParameters = new RSAParameters var rsaParameters = new RSAParameters