Transaction functionality complete.

This commit is contained in:
Andrey Shabarshov 2022-03-13 17:38:18 +00:00
parent 82b8d1820e
commit 1a89deb394
13 changed files with 179 additions and 114 deletions

View File

@ -2,35 +2,15 @@
{ {
public record ClientToMerchantResponse public record ClientToMerchantResponse
{ {
public MerchantToClientRequest MerchantRequest public MerchantToClientRequest MerchantRequest { get; set; }
{
get;
set;
}
public string ClientId public string ClientId { get; set; }
{
get;
set;
}
public DateTime TimeStampUTC public DateTime TimeStampUTC { get; set; }
{
get;
set;
}
public string ClientSignature public string ClientSignature { get; set; }
{
get;
set;
}
public string EncryptedClientCardData public string EncryptedClientCardData { get; set; }
{
get;
set;
}
/// <summary> /// <summary>
/// Convert ClientToMerchantResponse to string to be used as QR Code source (along with client signature) /// Convert ClientToMerchantResponse to string to be used as QR Code source (along with client signature)

View File

@ -4,41 +4,17 @@ namespace QRBee.Core.Data
{ {
public record MerchantToClientRequest public record MerchantToClientRequest
{ {
public string MerchantId public string MerchantId { get; set; }
{
get;
set;
}
public string MerchantTransactionId public string MerchantTransactionId { get; set; }
{
get;
set;
}
public string Name public string Name { get; set; }
{
get;
set;
}
public decimal Amount public decimal Amount { get; set; }
{
get;
set;
}
public DateTime TimeStampUTC public DateTime TimeStampUTC { get; set; }
{
get;
set;
}
public string MerchantSignature public string MerchantSignature { get; set; }
{
get;
set;
}
/// <summary> /// <summary>
/// Convert MerchantToClientRequest to string to be used as QR Code source (along with merchant signature) /// Convert MerchantToClientRequest to string to be used as QR Code source (along with merchant signature)

View File

@ -2,23 +2,22 @@
{ {
public record PaymentResponse public record PaymentResponse
{ {
public string ServerTransactionId { get; set; }
public string ServerTransactionId public PaymentRequest PaymentRequest { get; set; }
{
get;
set;
}
public PaymentRequest PaymentRequest public DateTime ServerTimeStampUTC { get; set; }
{
get; public bool Success { get; set; }
set;
} public string RejectReason { get; set; }
public string ServerSignature { get; set; }
/// <summary> /// <summary>
/// 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 AsString() => $"{ServerTransactionId}|{PaymentRequest.AsString()}"; public string AsDataForSignature() => $"{ServerTransactionId}|{PaymentRequest.AsString()}|{ServerTimeStampUTC:O}|{Success}|{RejectReason}";
} }
} }

View File

@ -4,35 +4,15 @@ namespace QRBee.Core.Data
{ {
public record RegistrationRequest public record RegistrationRequest
{ {
public string Name public string Name { get; set; }
{
get;
set;
}
public string Email public string Email { get; set; }
{
get;
set;
}
public string DateOfBirth public string DateOfBirth { get; set; }
{
get;
set;
}
public ReadableCertificateRequest CertificateRequest public ReadableCertificateRequest CertificateRequest { get; set; }
{
get;
set;
}
public bool RegisterAsMerchant public bool RegisterAsMerchant { get; set; }
{
get;
set;
}
} }

View File

@ -6,23 +6,11 @@ namespace QRBee.Core.Data
{ {
public record RegistrationResponse public record RegistrationResponse
{ {
public string ClientId public string ClientId { get; set; }
{
get;
set;
}
public string ClientCertificate public string ClientCertificate { get; set; }
{
get;
set;
}
public string APIServerCertificate public string APIServerCertificate { get; set; }
{
get;
set;
}
} }
} }

View File

@ -50,9 +50,30 @@ namespace QRBee.ViewModels
var response = await service.PayAsync(paymentRequest); var response = await service.PayAsync(paymentRequest);
//TODO handle response if (response.Success)
{
var check = _securityService.Verify(
Encoding.UTF8.GetBytes(response.AsDataForSignature()),
Convert.FromBase64String(response.ServerSignature),
_securityService.APIServerCertificate
);
if (check)
{
await Application.Current.MainPage.DisplayAlert("Success", "The transaction completed successfully ", "Ok"); await Application.Current.MainPage.DisplayAlert("Success", "The transaction completed successfully ", "Ok");
} }
else
{
await Application.Current.MainPage.DisplayAlert("Failure", "Invalid server signature", "Ok");
}
}
else
{
await Application.Current.MainPage.DisplayAlert("Failure", $"The transaction failed: {response.RejectReason}", "Ok");
}
}
catch (Exception e) catch (Exception e)
{ {
//TODO: delete exception message in error message //TODO: delete exception message in error message

View File

@ -31,6 +31,7 @@ builder.Services
.AddSingleton<IMongoClient>( cfg => new MongoClient(cfg.GetRequiredService<IOptions<DatabaseSettings>>().Value.ToMongoDbSettings())) .AddSingleton<IMongoClient>( cfg => new MongoClient(cfg.GetRequiredService<IOptions<DatabaseSettings>>().Value.ToMongoDbSettings()))
.AddSingleton<IPrivateKeyHandler, ServerPrivateKeyHandler>() .AddSingleton<IPrivateKeyHandler, ServerPrivateKeyHandler>()
.AddSingleton<ISecurityService, SecurityService>() .AddSingleton<ISecurityService, SecurityService>()
.AddSingleton<IPaymentGateway, PaymentGateway>()
; ;
var app = builder.Build(); var app = builder.Build();

View File

@ -38,6 +38,12 @@
/// <returns>Transaction information</returns> /// <returns>Transaction information</returns>
Task<TransactionInfo> GetTransactionInfoByTransactionId(string id); Task<TransactionInfo> GetTransactionInfoByTransactionId(string id);
/// <summary>
/// Update transaction after execution
/// </summary>
/// <param name="info">Transaction to be updated</param>
Task UpdateTransaction(TransactionInfo info);
/// <summary> /// <summary>
/// Inserts CertificateInfo into database /// Inserts CertificateInfo into database
/// </summary> /// </summary>

View File

@ -93,7 +93,7 @@ namespace QRBee.Api.Services.Database
private async Task<TransactionInfo?> TryGetTransactionInfo(string id) private async Task<TransactionInfo?> TryGetTransactionInfo(string id)
{ {
var collection = _database.GetCollection<TransactionInfo>("Transactions"); var collection = _database.GetCollection<TransactionInfo>("Transactions");
using var cursor = await collection.FindAsync($"{{ Id: \"{id}\" }}"); using var cursor = await collection.FindAsync($"{{ _id: \"{id}\" }}");
if (!await cursor.MoveNextAsync()) if (!await cursor.MoveNextAsync())
{ {
return null; return null;
@ -108,6 +108,12 @@ 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 UpdateTransaction(TransactionInfo info)
{
var collection = _database.GetCollection<TransactionInfo>("Transactions");
await collection.ReplaceOneAsync($"{{ _id: \"{info.Id}\" }}", info, new ReplaceOptions() { IsUpsert = false });
}
public async Task InsertCertificate(CertificateInfo info) public async Task InsertCertificate(CertificateInfo info)
{ {
var collection = _database.GetCollection<CertificateInfo>("Certificates"); var collection = _database.GetCollection<CertificateInfo>("Certificates");

View File

@ -41,5 +41,7 @@ namespace QRBee.Api.Services.Database
Succeeded = 2, Succeeded = 2,
} }
public TransactionStatus Status { get; set; } = TransactionStatus.Pending; public TransactionStatus Status { get; set; } = TransactionStatus.Pending;
public string? RejectReason { get; set; }
} }
} }

View File

@ -0,0 +1,17 @@
using QRBee.Api.Services.Database;
using QRBee.Core.Data;
namespace QRBee.Api.Services
{
public class GatewayResponse
{
public bool Success { get; init; }
public string? ErrorMessage { get; init; }
}
public interface IPaymentGateway
{
Task<GatewayResponse> Payment(TransactionInfo info, ClientCardData clientCardData);
}
}

View File

@ -0,0 +1,32 @@
using QRBee.Api.Services.Database;
using QRBee.Core.Data;
namespace QRBee.Api.Services;
internal class PaymentGateway : IPaymentGateway
{
private readonly ILogger<Storage> _logger;
public PaymentGateway(ILogger<Storage> logger)
{
_logger = logger;
}
public Task<GatewayResponse> Payment(TransactionInfo info, ClientCardData clientCardData)
{
if (info.Request.ClientResponse.MerchantRequest.Amount < 10)
{
_logger.LogInformation($"Transaction with id: {info.Id} failed");
return Task.FromResult(new GatewayResponse
{
Success = false,
ErrorMessage = "Amount is too low"
});
}
_logger.LogInformation($"Transaction with id: {info.Id} succeeded");
return Task.FromResult(new GatewayResponse
{
Success = true
});
}
}

View File

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
@ -18,16 +19,18 @@ namespace QRBee.Api.Services
private readonly IStorage _storage; private readonly IStorage _storage;
private readonly ISecurityService _securityService; private readonly ISecurityService _securityService;
private readonly IPrivateKeyHandler _privateKeyHandler; private readonly IPrivateKeyHandler _privateKeyHandler;
private readonly IPaymentGateway _paymentGateway;
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) public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler, IPaymentGateway paymentGateway)
{ {
_storage = storage; _storage = storage;
_securityService = securityService; _securityService = securityService;
_privateKeyHandler = privateKeyHandler; _privateKeyHandler = privateKeyHandler;
_paymentGateway = paymentGateway;
Init(_privateKeyHandler); Init(_privateKeyHandler);
} }
@ -150,13 +153,42 @@ namespace QRBee.Api.Services
var clientCardData = DecryptClientData(value.ClientResponse.EncryptedClientCardData); var clientCardData = DecryptClientData(value.ClientResponse.EncryptedClientCardData);
//6. Check client card data for validity //6. Check client card data for validity
await CheckClientCardData(clientCardData);
//7. Register preliminary transaction record with expiry of one minute //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); var info = Convert(value);
info.Status = TransactionInfo.TransactionStatus.Pending;
await _storage.PutTransactionInfo(info); await _storage.PutTransactionInfo(info);
return new PaymentResponse();
//8. Send client card data to a payment gateway
var res = await _paymentGateway.Payment(info, clientCardData);
//9. Record transaction with result
if (res.Success)
{
info.Status=TransactionInfo.TransactionStatus.Succeeded;
}
else
{
info.Status = TransactionInfo.TransactionStatus.Rejected;
info.RejectReason = res.ErrorMessage;
}
await _storage.UpdateTransaction(info);
//10. Make response for merchant
var response = new PaymentResponse
{
ServerTransactionId = info.TransactionId,
PaymentRequest = value,
ServerTimeStampUTC = DateTime.UtcNow,
Success = res.Success,
RejectReason = res.ErrorMessage,
};
var signature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ServerSignature = System.Convert.ToBase64String(signature);
return response;
} }
private void ValidateTransaction(PaymentRequest request) private void ValidateTransaction(PaymentRequest request)
@ -222,6 +254,31 @@ namespace QRBee.Api.Services
return ClientCardData.FromString(s); return ClientCardData.FromString(s);
} }
private async Task CheckClientCardData(ClientCardData data)
{
var transactionId = data.TransactionId;
var expirationDate = DateTime.Parse(data.ExpirationDateMMYY);
var validFrom = DateTime.Parse(data.ValidFrom);
var holderName = data.CardHolderName;
await CheckTransaction(transactionId);
if (expirationDate <= DateTime.UtcNow)
{
throw new ApplicationException($"The expiration date: {expirationDate} is wrong");
}
if (validFrom > DateTime.UtcNow)
{
throw new ApplicationException($"The valid from date: {validFrom} is wrong");
}
if (holderName.Any(char.IsDigit))
{
throw new ApplicationException($"The card holder name: {holderName} is wrong");
}
}
private static RSA LoadRsaPublicKey(StringRSAParameters stringParameters) private static RSA LoadRsaPublicKey(StringRSAParameters stringParameters)
{ {
var rsaParameters = new RSAParameters var rsaParameters = new RSAParameters