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 MerchantToClientRequest MerchantRequest
{
get;
set;
}
public MerchantToClientRequest MerchantRequest { get; set; }
public string ClientId
{
get;
set;
}
public string ClientId { get; set; }
public DateTime TimeStampUTC
{
get;
set;
}
public DateTime TimeStampUTC { get; set; }
public string ClientSignature
{
get;
set;
}
public string ClientSignature { get; set; }
public string EncryptedClientCardData
{
get;
set;
}
public string EncryptedClientCardData { get; set; }
/// <summary>
/// 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 string MerchantId
{
get;
set;
}
public string MerchantId { get; set; }
public string MerchantTransactionId
{
get;
set;
}
public string MerchantTransactionId { get; set; }
public string Name
{
get;
set;
}
public string Name { get; set; }
public decimal Amount
{
get;
set;
}
public decimal Amount { get; set; }
public DateTime TimeStampUTC
{
get;
set;
}
public DateTime TimeStampUTC { get; set; }
public string MerchantSignature
{
get;
set;
}
public string MerchantSignature { get; set; }
/// <summary>
/// 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 string ServerTransactionId { get; set; }
public string ServerTransactionId
{
get;
set;
}
public PaymentRequest PaymentRequest { get; set; }
public PaymentRequest PaymentRequest
{
get;
set;
}
public DateTime ServerTimeStampUTC { get; set; }
public bool Success { get; set; }
public string RejectReason { get; set; }
public string ServerSignature { get; set; }
/// <summary>
/// Convert PaymentResponse to string to be encrypted and transmitted back to merchant
/// </summary>
/// <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 string Name
{
get;
set;
}
public string Name { get; set; }
public string Email
{
get;
set;
}
public string Email { get; set; }
public string DateOfBirth
{
get;
set;
}
public string DateOfBirth { get; set; }
public ReadableCertificateRequest CertificateRequest
{
get;
set;
}
public ReadableCertificateRequest CertificateRequest { get; set; }
public bool RegisterAsMerchant
{
get;
set;
}
public bool RegisterAsMerchant { get; set; }
}

View File

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

View File

@ -50,9 +50,30 @@ namespace QRBee.ViewModels
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");
}
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)
{
//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<IPrivateKeyHandler, ServerPrivateKeyHandler>()
.AddSingleton<ISecurityService, SecurityService>()
.AddSingleton<IPaymentGateway, PaymentGateway>()
;
var app = builder.Build();

View File

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

View File

@ -93,7 +93,7 @@ namespace QRBee.Api.Services.Database
private async Task<TransactionInfo?> TryGetTransactionInfo(string id)
{
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())
{
return null;
@ -108,6 +108,12 @@ namespace QRBee.Api.Services.Database
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)
{
var collection = _database.GetCollection<CertificateInfo>("Certificates");

View File

@ -41,5 +41,7 @@ namespace QRBee.Api.Services.Database
Succeeded = 2,
}
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.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
@ -18,16 +19,18 @@ namespace QRBee.Api.Services
private readonly IStorage _storage;
private readonly ISecurityService _securityService;
private readonly IPrivateKeyHandler _privateKeyHandler;
private readonly IPaymentGateway _paymentGateway;
private static readonly object _lock = new ();
private const int MaxNameLength = 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;
_securityService = securityService;
_privateKeyHandler = privateKeyHandler;
_paymentGateway = paymentGateway;
Init(_privateKeyHandler);
}
@ -150,13 +153,42 @@ namespace QRBee.Api.Services
var clientCardData = DecryptClientData(value.ClientResponse.EncryptedClientCardData);
//6. Check client card data for validity
await CheckClientCardData(clientCardData);
//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);
info.Status = TransactionInfo.TransactionStatus.Pending;
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)
@ -222,6 +254,31 @@ namespace QRBee.Api.Services
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)
{
var rsaParameters = new RSAParameters