Payment transaction fixed.

This commit is contained in:
Andrey Shabarshov 2022-03-30 12:34:20 +01:00
parent 01cafaba14
commit 400a41760b
10 changed files with 167 additions and 127 deletions

View File

@ -4,8 +4,8 @@
{
public string TransactionId { get; set; }
public string CardNumber { get; set; }
public string ExpirationDateMMYY { get; set; }
public string ValidFrom { get; set; }
public string ExpirationDateYYYYMM { get; set; }
public string ValidFromYYYYMM { get; set; }
public string CardHolderName { get; set; }
public string CVC { get; set; }
public int? IssueNo { get; set; }
@ -15,7 +15,7 @@
/// WARNING: this should always be encrypted and never transmitted in clear text form.
/// </summary>
/// <returns>Converted string</returns>
public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo ?? 0}";
public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateYYYYMM}|{ValidFromYYYYMM}|{CardHolderName}|{CVC}|{IssueNo ?? 0}";
public static ClientCardData FromString(string input)
{
@ -27,12 +27,12 @@
var res = new ClientCardData()
{
TransactionId = s[0],
CardNumber = s[1],
ExpirationDateMMYY = s[2],
ValidFrom = s[3],
CardHolderName = s[4],
CVC = s[5]
TransactionId = s[0],
CardNumber = s[1],
ExpirationDateYYYYMM = s[2],
ValidFromYYYYMM = s[3],
CardHolderName = s[4],
CVC = s[5]
};
if (!string.IsNullOrWhiteSpace(s[6]))

View File

@ -3,22 +3,17 @@
public record ClientToMerchantResponse
{
public MerchantToClientRequest MerchantRequest { get; set; }
public string ClientId { get; set; }
public DateTime TimeStampUTC { get; set; }
public string ClientSignature { get; set; }
public string EncryptedClientCardData { get; set; }
public string ClientId { get; set; }
public DateTime TimeStampUTC { get; set; }
public string ClientSignature { get; set; }
public string EncryptedClientCardData { get; set; }
/// <summary>
/// Convert ClientToMerchantResponse to string to be used as QR Code source (along with client signature)
/// </summary>
/// <returns> Converted string</returns>
public string AsQRCodeString() => $"{ClientId}|{TimeStampUTC:O}|{ClientSignature}";
public string AsDataForSignature() => $"{ClientId}|{TimeStampUTC:O}|{MerchantRequest.AsQRCodeString()}";
public string AsQRCodeString() => $"{ClientId}|{TimeStampUTC:yyyy-MM-dd:HH.mm.ss.ffff}|{ClientSignature}|{EncryptedClientCardData}";
public string AsDataForSignature() => $"{ClientId}|{TimeStampUTC:yyyy-MM-dd:HH.mm.ss.ffff}|{MerchantRequest.AsQRCodeString()}";
/// <summary>
/// Convert from string
@ -26,19 +21,26 @@
/// <param name="input">A string representation of ClientToMerchantResponse</param>
/// <returns>Converted string</returns>
/// <exception cref="ApplicationException">Thrown if the input string is incorrect</exception>
public static ClientToMerchantResponse FromString(string input)
public static ClientToMerchantResponse FromString(string input, MerchantToClientRequest merchantRequest)
{
if (merchantRequest == null)
throw new ArgumentNullException(nameof(merchantRequest));
if ( string.IsNullOrWhiteSpace(merchantRequest.MerchantSignature) )
throw new ApplicationException("Request is not signed by a merchant");
var s = input.Split('|');
if (s.Length < 3)
if (s.Length < 4)
{
throw new ApplicationException($"Expected 3 or more elements but got {s.Length}");
throw new ApplicationException($"Expected 4 elements but got {s.Length}");
}
var res = new ClientToMerchantResponse()
{
ClientId = s[0],
TimeStampUTC = DateTime.ParseExact(s[1], "O", null),
ClientSignature = s[2]
ClientId = s[0],
TimeStampUTC = DateTime.ParseExact(s[1], "yyyy-MM-dd:HH.mm.ss.ffff", null),
ClientSignature = s[2],
EncryptedClientCardData = s[3],
MerchantRequest = merchantRequest
};
return res;

View File

@ -4,25 +4,19 @@ namespace QRBee.Core.Data
{
public record MerchantToClientRequest
{
public string MerchantId { get; set; }
public string MerchantId { get; set; }
public string MerchantTransactionId { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime TimeStampUTC { get; set; }
public string MerchantSignature { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime TimeStampUTC { get; set; }
public string MerchantSignature { get; set; }
/// <summary>
/// Convert MerchantToClientRequest to string to be used as QR Code source (along with merchant signature)
/// </summary>
/// <returns>String conversion</returns>
public string AsQRCodeString() => $"{AsDataForSignature()}|{MerchantSignature}";
public string AsDataForSignature() => $"{MerchantId}|{MerchantTransactionId}|{Name}|{Amount.ToString("0.00", CultureInfo.InvariantCulture)}|{TimeStampUTC:O}";
public string AsQRCodeString() => $"{AsDataForSignature()}|{MerchantSignature}";
public string AsDataForSignature() => $"{MerchantId}|{MerchantTransactionId}|{Name}|{Amount.ToString("0.00", CultureInfo.InvariantCulture)}|{TimeStampUTC:yyyy-MM-dd:HH.mm.ss.ffff}";
/// <summary>
/// Convert from string
@ -40,12 +34,12 @@ namespace QRBee.Core.Data
var res = new MerchantToClientRequest
{
MerchantId = s[0],
MerchantId = s[0],
MerchantTransactionId = s[1],
Name = s[2],
Amount = Convert.ToDecimal(s[3], CultureInfo.InvariantCulture),
TimeStampUTC = DateTime.ParseExact(s[4],"O",null),
MerchantSignature = s[5]
Name = s[2],
Amount = Convert.ToDecimal(s[3], CultureInfo.InvariantCulture),
TimeStampUTC = DateTime.ParseExact(s[4], "yyyy-MM-dd:HH.mm.ss.ffff", null),
MerchantSignature = s[5]
};

View File

@ -18,6 +18,6 @@
/// Convert PaymentResponse to string to be encrypted and transmitted back to merchant
/// </summary>
/// <returns>Converted string</returns>
public string AsDataForSignature() => $"{ServerTransactionId}|{PaymentRequest.AsString()}|{ServerTimeStampUTC:O}|{Success}|{RejectReason}";
public string AsDataForSignature() => $"{ServerTransactionId}|{PaymentRequest.AsString()}|{ServerTimeStampUTC:yyyy-MM-dd:HH.mm.ss.ffff}|{Success}|{RejectReason}";
}
}

View File

@ -61,10 +61,10 @@ namespace QRBee.ViewModels
if (result == null)
return;
_merchantToClientRequest = MerchantToClientRequest.FromString(result);
Amount = $"{_merchantToClientRequest.Amount:N2}";
_merchantToClientRequest = MerchantToClientRequest.FromString(result);
Amount = $"{_merchantToClientRequest.Amount:N2}";
IsAcceptDenyButtonVisible = true;
IsScanButtonVisible = false;
IsScanButtonVisible = false;
}
catch (Exception)
{
@ -148,36 +148,40 @@ namespace QRBee.ViewModels
public async void OnAcceptQrCommand(object obj)
{
var answer = await Application.Current.MainPage.DisplayAlert("Confirmation", "Would you like to accept the offer?", "Yes", "No");
if (!answer) return;
var answer = await Application.Current.MainPage.DisplayAlert("Confirmation", "Would you like to accept the offer?", "Yes", "No");
if (!answer)
return;
var settings = _localSettings.LoadSettings();
var response = new ClientToMerchantResponse
{
ClientId = settings.ClientId,
TimeStampUTC = DateTime.UtcNow,
MerchantRequest = _merchantToClientRequest,
ClientId = settings.ClientId,
TimeStampUTC = DateTime.UtcNow,
MerchantRequest = _merchantToClientRequest,
EncryptedClientCardData = EncryptCardData(settings, _merchantToClientRequest.MerchantTransactionId)
};
var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ClientSignature = Convert.ToBase64String(clientSignature);
QrCode = response.AsQRCodeString();
IsQrVisible = true;
var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ClientSignature = Convert.ToBase64String(clientSignature);
QrCode = response.AsQRCodeString();
IsQrVisible = true;
IsAcceptDenyButtonVisible = false;
IsScanButtonVisible = true;
IsScanButtonVisible = true;
}
private string EncryptCardData(Settings settings, string transactionId)
{
var clientCardData = new ClientCardData
{
TransactionId = transactionId,
CardNumber = settings.CardNumber,
ExpirationDateMMYY = settings.ExpirationDate,
ValidFrom = settings.ValidFrom,
CardHolderName = settings.CardHolderName,
CVC = settings.CVC,
IssueNo = settings.IssueNo
TransactionId = transactionId,
CardNumber = settings.CardNumber,
ExpirationDateYYYYMM = string.IsNullOrWhiteSpace(settings.ExpirationDate) ? null : DateTime.Parse(settings.ExpirationDate).ToString("yyyy-MM"),
ValidFromYYYYMM = string.IsNullOrWhiteSpace(settings.ValidFrom) ? null : DateTime.Parse(settings.ValidFrom).ToString("yyyy-MM"),
CardHolderName = settings.CardHolderName,
CVC = settings.CVC,
IssueNo = settings.IssueNo
};
var bytes = _securityService.Encrypt(Encoding.UTF8.GetBytes(clientCardData.AsString()),_securityService.APIServerCertificate);

View File

@ -24,38 +24,51 @@ namespace QRBee.ViewModels
public MerchantPageViewModel(IQRScanner scanner, ILocalSettings settings, ISecurityService securityService)
{
_scanner = scanner;
_settings = settings;
_securityService = securityService;
ScanCommand = new Command(OnScanButtonClicked);
_scanner = scanner;
_settings = settings;
_securityService = securityService;
ScanCommand = new Command(OnScanButtonClicked);
GenerateQrCommand = new Command(OnGenerateQrClicked);
var localSettings = DependencyService.Resolve<ILocalSettings>();
Name = localSettings.LoadSettings().Name;
Name = localSettings.LoadSettings().Name;
}
private async void OnScanButtonClicked(object sender)
{
try
{
var result = await _scanner.ScanQR();
if (result == null)
var result = await _scanner.ScanQR();
if (string.IsNullOrWhiteSpace(result))
return;
var client = new HttpClient(GetInsecureHandler());
var clientResponse = ClientToMerchantResponse.FromString(result, _lastRequest);
var service = new Core.Client.Client(_settings.QRBeeApiUrl, client);
var clientResponse = ClientToMerchantResponse.FromString(result);
if (string.IsNullOrWhiteSpace(clientResponse.ClientSignature))
throw new ApplicationException("Request is not signed by a client");
if (string.IsNullOrWhiteSpace(clientResponse.EncryptedClientCardData))
throw new ApplicationException("Request does not contain client's card data");
clientResponse.MerchantRequest = _lastRequest;
var paymentRequest = new PaymentRequest
var paymentRequest = new PaymentRequest
{
ClientResponse = clientResponse
ClientResponse = clientResponse
};
//QrCode = null;
IsVisible = false;
var response = await service.PayAsync(paymentRequest);
// ------------------------------------- SEND PAYMENT REQUEST ------------------------------------------
//
// ____ _ __ ____ __ _____ _ _ _____
// | _ \ / \\ \ / / \/ | ____| \ | |_ _|
// | |_) / _ \\ V /| |\/| | _| | \| | | |
// | __/ ___ \| | | | | | |___| |\ | | |
// |_| /_/ \_\_| |_| |_|_____|_| \_| |_|
//
//
var apiService = new Core.Client.Client(_settings.QRBeeApiUrl, new HttpClient(GetInsecureHandler()));
var response = await apiService.PayAsync(paymentRequest);
//
// -----------------------------------------------------------------------------------------------------
if (response.Success)
{

View File

@ -31,6 +31,12 @@
/// <param name="info">Information to be inserted</param>
Task PutTransactionInfo(TransactionInfo info);
/// <summary>
/// Try to find if the Transaction already exists in the database
/// </summary>
/// <param name="id">parameter by which to find TransactionInfo</param>
/// <returns>null if transaction doesn't exist or TransactionInfo</returns>
Task<TransactionInfo?> TryGetTransactionInfoByTransactionId(string id);
/// <summary>
/// Retrieve transaction information from database
/// </summary>

View File

@ -71,7 +71,7 @@ namespace QRBee.Api.Services.Database
{
var collection = _database.GetCollection<TransactionInfo>("Transactions");
var transaction = await TryGetTransactionInfo(info.Id);
var transaction = await TryGetTransactionInfoByTransactionId(info.Id);
if (transaction == null)
{
@ -83,12 +83,7 @@ namespace QRBee.Api.Services.Database
_logger.LogInformation($"Found transaction with ClientId: {info.Id}");
}
/// <summary>
/// Try to find if the Transaction already exists in the database
/// </summary>
/// <param name="id">parameter by which to find TransactionInfo</param>
/// <returns>null if transaction doesn't exist or TransactionInfo</returns>
private async Task<TransactionInfo?> TryGetTransactionInfo(string id)
public async Task<TransactionInfo?> TryGetTransactionInfoByTransactionId(string id)
{
var collection = _database.GetCollection<TransactionInfo>("Transactions");
using var cursor = await collection.FindAsync($"{{ _id: \"{id}\" }}");
@ -102,7 +97,7 @@ namespace QRBee.Api.Services.Database
public async Task<TransactionInfo> GetTransactionInfoByTransactionId(string id)
{
var transaction = await TryGetTransactionInfo(id);
var transaction = await TryGetTransactionInfoByTransactionId(id);
return transaction ?? throw new ApplicationException($"Transaction with Id: {id} not found.");
}

View File

@ -14,21 +14,23 @@ namespace QRBee.Api.Services
/// </summary>
public class QRBeeAPIService: IQRBeeAPI
{
private readonly IStorage _storage;
private readonly ISecurityService _securityService;
private readonly IStorage _storage;
private readonly ISecurityService _securityService;
private readonly IPrivateKeyHandler _privateKeyHandler;
private readonly IPaymentGateway _paymentGateway;
private static readonly object _lock = new ();
private readonly IPaymentGateway _paymentGateway;
private readonly ILogger<QRBeeAPIService> _logger;
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)
public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler, IPaymentGateway paymentGateway, ILogger<QRBeeAPIService> logger)
{
_storage = storage;
_securityService = securityService;
_storage = storage;
_securityService = securityService;
_privateKeyHandler = privateKeyHandler;
_paymentGateway = paymentGateway;
_paymentGateway = paymentGateway;
_logger = logger;
Init(_privateKeyHandler);
}
@ -48,14 +50,14 @@ namespace QRBee.Api.Services
ValidateRegistration(request);
var info = Convert(request);
var clientId = await _storage.PutUserInfo(info);
var info = Convert(request);
var clientId = await _storage.PutUserInfo(info);
using var rsa = LoadRsaPublicKey(request.CertificateRequest.RsaPublicKey);
var bytes = rsa.ExportRSAPublicKey();
var clientCertificate = _securityService.CreateCertificate(clientId,bytes);
var bytes = rsa.ExportRSAPublicKey();
var clientCertificate = _securityService.CreateCertificate(clientId,bytes);
var convertedClientCertificate = Convert(clientCertificate, clientId,request.Email);
await _storage.InsertCertificate(convertedClientCertificate);
@ -81,9 +83,9 @@ namespace QRBee.Api.Services
throw new NullReferenceException();
}
var name = request.Name;
var email = request.Email;
var dateOfBirth = request.DateOfBirth;
var name = request.Name;
var email = request.Email;
var dateOfBirth = request.DateOfBirth;
var certificateRequest = request.CertificateRequest;
if (string.IsNullOrEmpty(name) || name.All(char.IsLetter) == false || name.Length >= MaxNameLength)
@ -126,10 +128,23 @@ namespace QRBee.Api.Services
public async Task<PaymentResponse> Pay(PaymentRequest value)
{
// --------------------------------- RECEIVE PAYMENT REQUEST --------------------------------------
//
// ____ _ __ ____ __ _____ _ _ _____
// | _ \ / \\ \ / / \/ | ____| \ | |_ _|
// | |_) / _ \\ V /| |\/| | _| | \| | | |
// | __/ ___ \| | | | | | |___| |\ | | |
// |_| /_/ \_\_| |_| |_|_____|_| \_| |_|
//
//
try
{
//1. Check payment request parameters for validity
ValidateTransaction(value);
var tid = value.ClientResponse.MerchantRequest.MerchantTransactionId;
_logger.LogInformation($"Transaction=\"{tid}\" Pre-validated");
//2. Check client signature
var t2 = CheckSignature(
@ -144,22 +159,25 @@ namespace QRBee.Api.Services
value.ClientResponse.MerchantRequest.MerchantId);
//4. Check if transaction was already processed
var t4 = CheckTransaction(value.ClientResponse.MerchantRequest.MerchantTransactionId);
var t4 = CheckTransaction(tid);
//Parallel task execution
await Task.WhenAll(t2, t3, t4);
_logger.LogInformation($"Transaction=\"{tid}\" Fully validated");
//5. Decrypt client card data
var clientCardData = DecryptClientData(value.ClientResponse.EncryptedClientCardData);
//6. Check client card data for validity
await CheckClientCardData(clientCardData);
await CheckClientCardData(clientCardData, value.ClientResponse.MerchantRequest.MerchantTransactionId);
_logger.LogInformation($"Transaction=\"{tid}\" Client card data validated");
//7. Register preliminary transaction record with expiry of one minute
var info = Convert(value);
info.Status = TransactionInfo.TransactionStatus.Pending;
await _storage.PutTransactionInfo(info);
_logger.LogInformation($"Transaction=\"{tid}\" initialized");
//8. Send client card data to a payment gateway
var res = await _paymentGateway.Payment(info, clientCardData);
@ -175,6 +193,7 @@ namespace QRBee.Api.Services
info.RejectReason = res.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);
@ -194,14 +213,15 @@ namespace QRBee.Api.Services
var response = new PaymentResponse
{
ServerTransactionId = transactionId,
PaymentRequest = value,
ServerTimeStampUTC = DateTime.UtcNow,
Success = result,
RejectReason = errorMessage,
PaymentRequest = value,
ServerTimeStampUTC = DateTime.UtcNow,
Success = result,
RejectReason = errorMessage,
};
var signature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
var signature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ServerSignature = System.Convert.ToBase64String(signature);
return response;
}
@ -212,10 +232,10 @@ namespace QRBee.Api.Services
throw new NullReferenceException();
}
var clientId = request.ClientResponse.ClientId;
var merchantId = request.ClientResponse.MerchantRequest.MerchantId;
var clientId = request.ClientResponse.ClientId;
var merchantId = request.ClientResponse.MerchantRequest.MerchantId;
var transactionId = request.ClientResponse.MerchantRequest.MerchantTransactionId;
var amount = request.ClientResponse.MerchantRequest.Amount;
var amount = request.ClientResponse.MerchantRequest.Amount;
if (clientId == null || merchantId == null || transactionId == null)
{
@ -230,7 +250,7 @@ namespace QRBee.Api.Services
private async Task CheckSignature(string data,string signature, string id)
{
var info = await _storage.GetCertificateInfoByUserId(id);
var info = await _storage.GetCertificateInfoByUserId(id);
var certificate = _securityService.Deserialize(info.Certificate);
var check = _securityService.Verify(
@ -246,8 +266,8 @@ namespace QRBee.Api.Services
private async Task CheckTransaction(string transactionId)
{
var info = await _storage.GetTransactionInfoByTransactionId(transactionId);
switch (info.Status)
var info = await _storage.TryGetTransactionInfoByTransactionId(transactionId);
switch (info?.Status)
{
case TransactionInfo.TransactionStatus.Succeeded:
throw new ApplicationException($"Transaction with Id: {transactionId} was already made.");
@ -262,18 +282,24 @@ namespace QRBee.Api.Services
private ClientCardData DecryptClientData(string encryptedClientCardData)
{
var info = System.Convert.FromBase64String(encryptedClientCardData);
var info = System.Convert.FromBase64String(encryptedClientCardData);
var bytes = _securityService.Decrypt(info);
var s = Encoding.UTF8.GetString(bytes);
var s = Encoding.UTF8.GetString(bytes);
return ClientCardData.FromString(s);
}
private async Task CheckClientCardData(ClientCardData data)
private async Task CheckClientCardData(ClientCardData data, string merchantTransactionId)
{
var transactionId = data.TransactionId;
var expirationDate = DateTime.Parse(data.ExpirationDateMMYY);
var validFrom = DateTime.Parse(data.ValidFrom);
var holderName = data.CardHolderName;
if ( data.TransactionId != merchantTransactionId)
throw new ApplicationException($"Transaction IDs don't match");
//_logger.LogInformation(data.AsString());
var transactionId = data.TransactionId;
var expirationDate = string.IsNullOrWhiteSpace(data.ExpirationDateYYYYMM) ? default : DateTime.ParseExact(data.ExpirationDateYYYYMM, "yyyy-MM", null);
var validFrom = string.IsNullOrWhiteSpace(data.ValidFromYYYYMM) ? default : DateTime.ParseExact(data.ValidFromYYYYMM, "yyyy-MM", null);
var holderName = data.CardHolderName;
await CheckTransaction(transactionId);

View File

@ -12,8 +12,8 @@
"Logging": {
"LogLevel": {
"Default": "Trace"
//"Microsoft.AspNetCore": "Debug"
"Default": "Trace",
"Microsoft.AspNetCore": "Information"
}
},
"AllowedHosts": "*"