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)
{
@ -29,8 +29,8 @@
{
TransactionId = s[0],
CardNumber = s[1],
ExpirationDateMMYY = s[2],
ValidFrom = s[3],
ExpirationDateYYYYMM = s[2],
ValidFromYYYYMM = s[3],
CardHolderName = s[4],
CVC = s[5]
};

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; }
/// <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]
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

@ -5,15 +5,10 @@ namespace QRBee.Core.Data
public record MerchantToClientRequest
{
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; }
/// <summary>
@ -21,8 +16,7 @@ namespace QRBee.Core.Data
/// </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 AsDataForSignature() => $"{MerchantId}|{MerchantTransactionId}|{Name}|{Amount.ToString("0.00", CultureInfo.InvariantCulture)}|{TimeStampUTC:yyyy-MM-dd:HH.mm.ss.ffff}";
/// <summary>
/// Convert from string
@ -44,7 +38,7 @@ namespace QRBee.Core.Data
MerchantTransactionId = s[1],
Name = s[2],
Amount = Convert.ToDecimal(s[3], CultureInfo.InvariantCulture),
TimeStampUTC = DateTime.ParseExact(s[4],"O",null),
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

@ -149,8 +149,11 @@ namespace QRBee.ViewModels
{
var answer = await Application.Current.MainPage.DisplayAlert("Confirmation", "Would you like to accept the offer?", "Yes", "No");
if (!answer) return;
if (!answer)
return;
var settings = _localSettings.LoadSettings();
var response = new ClientToMerchantResponse
{
ClientId = settings.ClientId,
@ -158,6 +161,7 @@ namespace QRBee.ViewModels
MerchantRequest = _merchantToClientRequest,
EncryptedClientCardData = EncryptCardData(settings, _merchantToClientRequest.MerchantTransactionId)
};
var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ClientSignature = Convert.ToBase64String(clientSignature);
@ -173,8 +177,8 @@ namespace QRBee.ViewModels
{
TransactionId = transactionId,
CardNumber = settings.CardNumber,
ExpirationDateMMYY = settings.ExpirationDate,
ValidFrom = settings.ValidFrom,
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

View File

@ -38,15 +38,16 @@ namespace QRBee.ViewModels
try
{
var result = await _scanner.ScanQR();
if (result == null)
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
{
ClientResponse = clientResponse
@ -55,7 +56,19 @@ namespace QRBee.ViewModels
//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

@ -18,17 +18,19 @@ namespace QRBee.Api.Services
private readonly ISecurityService _securityService;
private readonly IPrivateKeyHandler _privateKeyHandler;
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;
_privateKeyHandler = privateKeyHandler;
_paymentGateway = paymentGateway;
_logger = logger;
Init(_privateKeyHandler);
}
@ -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);
@ -202,6 +221,7 @@ namespace QRBee.Api.Services
var signature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ServerSignature = System.Convert.ToBase64String(signature);
return response;
}
@ -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.");
@ -265,14 +285,20 @@ namespace QRBee.Api.Services
var info = System.Convert.FromBase64String(encryptedClientCardData);
var bytes = _securityService.Decrypt(info);
var s = Encoding.UTF8.GetString(bytes);
return ClientCardData.FromString(s);
}
private async Task CheckClientCardData(ClientCardData data)
private async Task CheckClientCardData(ClientCardData data, string merchantTransactionId)
{
if ( data.TransactionId != merchantTransactionId)
throw new ApplicationException($"Transaction IDs don't match");
//_logger.LogInformation(data.AsString());
var transactionId = data.TransactionId;
var expirationDate = DateTime.Parse(data.ExpirationDateMMYY);
var validFrom = DateTime.Parse(data.ValidFrom);
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": "*"