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 TransactionId { get; set; }
public string CardNumber { get; set; } public string CardNumber { get; set; }
public string ExpirationDateMMYY { get; set; } public string ExpirationDateYYYYMM { get; set; }
public string ValidFrom { get; set; } public string ValidFromYYYYMM { get; set; }
public string CardHolderName { get; set; } public string CardHolderName { get; set; }
public string CVC { get; set; } public string CVC { get; set; }
public int? IssueNo { get; set; } public int? IssueNo { get; set; }
@ -15,7 +15,7 @@
/// 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 ?? 0}"; public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateYYYYMM}|{ValidFromYYYYMM}|{CardHolderName}|{CVC}|{IssueNo ?? 0}";
public static ClientCardData FromString(string input) public static ClientCardData FromString(string input)
{ {
@ -29,8 +29,8 @@
{ {
TransactionId = s[0], TransactionId = s[0],
CardNumber = s[1], CardNumber = s[1],
ExpirationDateMMYY = s[2], ExpirationDateYYYYMM = s[2],
ValidFrom = s[3], ValidFromYYYYMM = s[3],
CardHolderName = s[4], CardHolderName = s[4],
CVC = s[5] CVC = s[5]
}; };

View File

@ -3,22 +3,17 @@
public record ClientToMerchantResponse 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> /// <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)
/// </summary> /// </summary>
/// <returns> Converted string</returns> /// <returns> Converted string</returns>
public string AsQRCodeString() => $"{ClientId}|{TimeStampUTC:O}|{ClientSignature}"; 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()}";
public string AsDataForSignature() => $"{ClientId}|{TimeStampUTC:O}|{MerchantRequest.AsQRCodeString()}";
/// <summary> /// <summary>
/// Convert from string /// Convert from string
@ -26,19 +21,26 @@
/// <param name="input">A string representation of ClientToMerchantResponse</param> /// <param name="input">A string representation of ClientToMerchantResponse</param>
/// <returns>Converted string</returns> /// <returns>Converted string</returns>
/// <exception cref="ApplicationException">Thrown if the input string is incorrect</exception> /// <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('|'); 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() var res = new ClientToMerchantResponse()
{ {
ClientId = s[0], ClientId = s[0],
TimeStampUTC = DateTime.ParseExact(s[1], "O", null), TimeStampUTC = DateTime.ParseExact(s[1], "yyyy-MM-dd:HH.mm.ss.ffff", null),
ClientSignature = s[2] ClientSignature = s[2],
EncryptedClientCardData = s[3],
MerchantRequest = merchantRequest
}; };
return res; return res;

View File

@ -5,15 +5,10 @@ namespace QRBee.Core.Data
public record MerchantToClientRequest 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> /// <summary>
@ -21,8 +16,7 @@ namespace QRBee.Core.Data
/// </summary> /// </summary>
/// <returns>String conversion</returns> /// <returns>String conversion</returns>
public string AsQRCodeString() => $"{AsDataForSignature()}|{MerchantSignature}"; 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}";
public string AsDataForSignature() => $"{MerchantId}|{MerchantTransactionId}|{Name}|{Amount.ToString("0.00", CultureInfo.InvariantCulture)}|{TimeStampUTC:O}";
/// <summary> /// <summary>
/// Convert from string /// Convert from string
@ -44,7 +38,7 @@ namespace QRBee.Core.Data
MerchantTransactionId = s[1], MerchantTransactionId = s[1],
Name = s[2], Name = s[2],
Amount = Convert.ToDecimal(s[3], CultureInfo.InvariantCulture), 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] MerchantSignature = s[5]
}; };

View File

@ -18,6 +18,6 @@
/// 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 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"); 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 settings = _localSettings.LoadSettings();
var response = new ClientToMerchantResponse var response = new ClientToMerchantResponse
{ {
ClientId = settings.ClientId, ClientId = settings.ClientId,
@ -158,6 +161,7 @@ namespace QRBee.ViewModels
MerchantRequest = _merchantToClientRequest, MerchantRequest = _merchantToClientRequest,
EncryptedClientCardData = EncryptCardData(settings, _merchantToClientRequest.MerchantTransactionId) EncryptedClientCardData = EncryptCardData(settings, _merchantToClientRequest.MerchantTransactionId)
}; };
var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature())); var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ClientSignature = Convert.ToBase64String(clientSignature); response.ClientSignature = Convert.ToBase64String(clientSignature);
@ -173,8 +177,8 @@ namespace QRBee.ViewModels
{ {
TransactionId = transactionId, TransactionId = transactionId,
CardNumber = settings.CardNumber, CardNumber = settings.CardNumber,
ExpirationDateMMYY = settings.ExpirationDate, ExpirationDateYYYYMM = string.IsNullOrWhiteSpace(settings.ExpirationDate) ? null : DateTime.Parse(settings.ExpirationDate).ToString("yyyy-MM"),
ValidFrom = settings.ValidFrom, ValidFromYYYYMM = string.IsNullOrWhiteSpace(settings.ValidFrom) ? null : DateTime.Parse(settings.ValidFrom).ToString("yyyy-MM"),
CardHolderName = settings.CardHolderName, CardHolderName = settings.CardHolderName,
CVC = settings.CVC, CVC = settings.CVC,
IssueNo = settings.IssueNo IssueNo = settings.IssueNo

View File

@ -38,15 +38,16 @@ namespace QRBee.ViewModels
try try
{ {
var result = await _scanner.ScanQR(); var result = await _scanner.ScanQR();
if (result == null) if (string.IsNullOrWhiteSpace(result))
return; return;
var client = new HttpClient(GetInsecureHandler()); var clientResponse = ClientToMerchantResponse.FromString(result, _lastRequest);
var service = new Core.Client.Client(_settings.QRBeeApiUrl, client); if (string.IsNullOrWhiteSpace(clientResponse.ClientSignature))
var clientResponse = ClientToMerchantResponse.FromString(result); 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
@ -55,7 +56,19 @@ namespace QRBee.ViewModels
//QrCode = null; //QrCode = null;
IsVisible = false; 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) if (response.Success)
{ {

View File

@ -31,6 +31,12 @@
/// <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>
/// 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> /// <summary>
/// Retrieve transaction information from database /// Retrieve transaction information from database
/// </summary> /// </summary>

View File

@ -71,7 +71,7 @@ namespace QRBee.Api.Services.Database
{ {
var collection = _database.GetCollection<TransactionInfo>("Transactions"); var collection = _database.GetCollection<TransactionInfo>("Transactions");
var transaction = await TryGetTransactionInfo(info.Id); var transaction = await TryGetTransactionInfoByTransactionId(info.Id);
if (transaction == null) if (transaction == null)
{ {
@ -83,12 +83,7 @@ namespace QRBee.Api.Services.Database
_logger.LogInformation($"Found transaction with ClientId: {info.Id}"); _logger.LogInformation($"Found transaction with ClientId: {info.Id}");
} }
/// <summary> public async Task<TransactionInfo?> TryGetTransactionInfoByTransactionId(string id)
/// 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)
{ {
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}\" }}");
@ -102,7 +97,7 @@ namespace QRBee.Api.Services.Database
public async Task<TransactionInfo> GetTransactionInfoByTransactionId(string id) 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."); 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 ISecurityService _securityService;
private readonly IPrivateKeyHandler _privateKeyHandler; private readonly IPrivateKeyHandler _privateKeyHandler;
private readonly IPaymentGateway _paymentGateway; private readonly IPaymentGateway _paymentGateway;
private readonly ILogger<QRBeeAPIService> _logger;
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, IPaymentGateway paymentGateway) public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler, IPaymentGateway paymentGateway, ILogger<QRBeeAPIService> logger)
{ {
_storage = storage; _storage = storage;
_securityService = securityService; _securityService = securityService;
_privateKeyHandler = privateKeyHandler; _privateKeyHandler = privateKeyHandler;
_paymentGateway = paymentGateway; _paymentGateway = paymentGateway;
_logger = logger;
Init(_privateKeyHandler); Init(_privateKeyHandler);
} }
@ -126,10 +128,23 @@ namespace QRBee.Api.Services
public async Task<PaymentResponse> Pay(PaymentRequest value) public async Task<PaymentResponse> Pay(PaymentRequest value)
{ {
// --------------------------------- RECEIVE PAYMENT REQUEST --------------------------------------
//
// ____ _ __ ____ __ _____ _ _ _____
// | _ \ / \\ \ / / \/ | ____| \ | |_ _|
// | |_) / _ \\ V /| |\/| | _| | \| | | |
// | __/ ___ \| | | | | | |___| |\ | | |
// |_| /_/ \_\_| |_| |_|_____|_| \_| |_|
//
//
try try
{ {
//1. Check payment request parameters for validity //1. Check payment request parameters for validity
ValidateTransaction(value); ValidateTransaction(value);
var tid = value.ClientResponse.MerchantRequest.MerchantTransactionId;
_logger.LogInformation($"Transaction=\"{tid}\" Pre-validated");
//2. Check client signature //2. Check client signature
var t2 = CheckSignature( var t2 = CheckSignature(
@ -144,22 +159,25 @@ namespace QRBee.Api.Services
value.ClientResponse.MerchantRequest.MerchantId); value.ClientResponse.MerchantRequest.MerchantId);
//4. Check if transaction was already processed //4. Check if transaction was already processed
var t4 = CheckTransaction(value.ClientResponse.MerchantRequest.MerchantTransactionId); var t4 = CheckTransaction(tid);
//Parallel task execution //Parallel task execution
await Task.WhenAll(t2, t3, t4); await Task.WhenAll(t2, t3, t4);
_logger.LogInformation($"Transaction=\"{tid}\" Fully validated");
//5. Decrypt client card data //5. Decrypt client card data
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); 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 //7. Register preliminary transaction record with expiry of one minute
var info = Convert(value); var info = Convert(value);
info.Status = TransactionInfo.TransactionStatus.Pending; info.Status = TransactionInfo.TransactionStatus.Pending;
await _storage.PutTransactionInfo(info); await _storage.PutTransactionInfo(info);
_logger.LogInformation($"Transaction=\"{tid}\" initialized");
//8. Send client card data to a payment gateway //8. Send client card data to a payment gateway
var res = await _paymentGateway.Payment(info, clientCardData); var res = await _paymentGateway.Payment(info, clientCardData);
@ -175,6 +193,7 @@ namespace QRBee.Api.Services
info.RejectReason = res.ErrorMessage; info.RejectReason = res.ErrorMessage;
} }
await _storage.UpdateTransaction(info); await _storage.UpdateTransaction(info);
_logger.LogInformation($"Transaction=\"{tid}\" complete Status=\"{info.Status}\"");
//10. Make response for merchant //10. Make response for merchant
var response = MakePaymentResponse(value, info.TransactionId ?? "", info.Status==TransactionInfo.TransactionStatus.Succeeded, info.RejectReason); 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())); var signature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ServerSignature = System.Convert.ToBase64String(signature); response.ServerSignature = System.Convert.ToBase64String(signature);
return response; return response;
} }
@ -246,8 +266,8 @@ namespace QRBee.Api.Services
private async Task CheckTransaction(string transactionId) private async Task CheckTransaction(string transactionId)
{ {
var info = await _storage.GetTransactionInfoByTransactionId(transactionId); var info = await _storage.TryGetTransactionInfoByTransactionId(transactionId);
switch (info.Status) switch (info?.Status)
{ {
case TransactionInfo.TransactionStatus.Succeeded: case TransactionInfo.TransactionStatus.Succeeded:
throw new ApplicationException($"Transaction with Id: {transactionId} was already made."); 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 info = System.Convert.FromBase64String(encryptedClientCardData);
var bytes = _securityService.Decrypt(info); var bytes = _securityService.Decrypt(info);
var s = Encoding.UTF8.GetString(bytes); var s = Encoding.UTF8.GetString(bytes);
return ClientCardData.FromString(s); 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 transactionId = data.TransactionId;
var expirationDate = DateTime.Parse(data.ExpirationDateMMYY); var expirationDate = string.IsNullOrWhiteSpace(data.ExpirationDateYYYYMM) ? default : DateTime.ParseExact(data.ExpirationDateYYYYMM, "yyyy-MM", null);
var validFrom = DateTime.Parse(data.ValidFrom); var validFrom = string.IsNullOrWhiteSpace(data.ValidFromYYYYMM) ? default : DateTime.ParseExact(data.ValidFromYYYYMM, "yyyy-MM", null);
var holderName = data.CardHolderName; var holderName = data.CardHolderName;
await CheckTransaction(transactionId); await CheckTransaction(transactionId);

View File

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