diff --git a/QRBee.Core/Data/ClientToMerchantResponse.cs b/QRBee.Core/Data/ClientToMerchantResponse.cs index cb2a8d8..8a41558 100644 --- a/QRBee.Core/Data/ClientToMerchantResponse.cs +++ b/QRBee.Core/Data/ClientToMerchantResponse.cs @@ -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; } /// /// Convert ClientToMerchantResponse to string to be used as QR Code source (along with client signature) diff --git a/QRBee.Core/Data/MerchantToClientRequest.cs b/QRBee.Core/Data/MerchantToClientRequest.cs index 6f843ce..ae8dfaf 100644 --- a/QRBee.Core/Data/MerchantToClientRequest.cs +++ b/QRBee.Core/Data/MerchantToClientRequest.cs @@ -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; } /// /// Convert MerchantToClientRequest to string to be used as QR Code source (along with merchant signature) diff --git a/QRBee.Core/Data/PaymentResponse.cs b/QRBee.Core/Data/PaymentResponse.cs index 0da5777..c41563c 100644 --- a/QRBee.Core/Data/PaymentResponse.cs +++ b/QRBee.Core/Data/PaymentResponse.cs @@ -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; } /// /// Convert PaymentResponse to string to be encrypted and transmitted back to merchant /// /// Converted string - public string AsString() => $"{ServerTransactionId}|{PaymentRequest.AsString()}"; + public string AsDataForSignature() => $"{ServerTransactionId}|{PaymentRequest.AsString()}|{ServerTimeStampUTC:O}|{Success}|{RejectReason}"; } } diff --git a/QRBee.Core/Data/RegistrationRequest.cs b/QRBee.Core/Data/RegistrationRequest.cs index 3f77c5d..b3a4d05 100644 --- a/QRBee.Core/Data/RegistrationRequest.cs +++ b/QRBee.Core/Data/RegistrationRequest.cs @@ -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; } } diff --git a/QRBee.Core/Data/RegistrationResponse.cs b/QRBee.Core/Data/RegistrationResponse.cs index 602d479..59d0f49 100644 --- a/QRBee.Core/Data/RegistrationResponse.cs +++ b/QRBee.Core/Data/RegistrationResponse.cs @@ -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; } } } diff --git a/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs b/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs index 56b7639..d59b077 100644 --- a/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs +++ b/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs @@ -50,8 +50,29 @@ namespace QRBee.ViewModels var response = await service.PayAsync(paymentRequest); - //TODO handle response - await Application.Current.MainPage.DisplayAlert("Success", "The transaction completed successfully ", "Ok"); + 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) { diff --git a/QRBeeApi/Program.cs b/QRBeeApi/Program.cs index 8bd8199..61a6167 100644 --- a/QRBeeApi/Program.cs +++ b/QRBeeApi/Program.cs @@ -31,6 +31,7 @@ builder.Services .AddSingleton( cfg => new MongoClient(cfg.GetRequiredService>().Value.ToMongoDbSettings())) .AddSingleton() .AddSingleton() + .AddSingleton() ; var app = builder.Build(); diff --git a/QRBeeApi/Services/Database/IStorage.cs b/QRBeeApi/Services/Database/IStorage.cs index 732932b..cdc4527 100644 --- a/QRBeeApi/Services/Database/IStorage.cs +++ b/QRBeeApi/Services/Database/IStorage.cs @@ -38,6 +38,12 @@ /// Transaction information Task GetTransactionInfoByTransactionId(string id); + /// + /// Update transaction after execution + /// + /// Transaction to be updated + Task UpdateTransaction(TransactionInfo info); + /// /// Inserts CertificateInfo into database /// diff --git a/QRBeeApi/Services/Database/Storage.cs b/QRBeeApi/Services/Database/Storage.cs index dbbc0bf..44de1a9 100644 --- a/QRBeeApi/Services/Database/Storage.cs +++ b/QRBeeApi/Services/Database/Storage.cs @@ -93,7 +93,7 @@ namespace QRBee.Api.Services.Database private async Task TryGetTransactionInfo(string id) { var collection = _database.GetCollection("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("Transactions"); + await collection.ReplaceOneAsync($"{{ _id: \"{info.Id}\" }}", info, new ReplaceOptions() { IsUpsert = false }); + } + public async Task InsertCertificate(CertificateInfo info) { var collection = _database.GetCollection("Certificates"); diff --git a/QRBeeApi/Services/Database/TransactionInfo.cs b/QRBeeApi/Services/Database/TransactionInfo.cs index be05600..9339271 100644 --- a/QRBeeApi/Services/Database/TransactionInfo.cs +++ b/QRBeeApi/Services/Database/TransactionInfo.cs @@ -41,5 +41,7 @@ namespace QRBee.Api.Services.Database Succeeded = 2, } public TransactionStatus Status { get; set; } = TransactionStatus.Pending; + + public string? RejectReason { get; set; } } } diff --git a/QRBeeApi/Services/IPaymentGateway.cs b/QRBeeApi/Services/IPaymentGateway.cs new file mode 100644 index 0000000..9603997 --- /dev/null +++ b/QRBeeApi/Services/IPaymentGateway.cs @@ -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 Payment(TransactionInfo info, ClientCardData clientCardData); + } +} diff --git a/QRBeeApi/Services/PaymentGateway.cs b/QRBeeApi/Services/PaymentGateway.cs new file mode 100644 index 0000000..0d48c74 --- /dev/null +++ b/QRBeeApi/Services/PaymentGateway.cs @@ -0,0 +1,32 @@ +using QRBee.Api.Services.Database; +using QRBee.Core.Data; + +namespace QRBee.Api.Services; + +internal class PaymentGateway : IPaymentGateway +{ + private readonly ILogger _logger; + + public PaymentGateway(ILogger logger) + { + _logger = logger; + } + + public Task 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 + }); + } +} \ No newline at end of file diff --git a/QRBeeApi/Services/QRBeeAPIService.cs b/QRBeeApi/Services/QRBeeAPIService.cs index 2c0a1bb..01ef060 100644 --- a/QRBeeApi/Services/QRBeeAPIService.cs +++ b/QRBeeApi/Services/QRBeeAPIService.cs @@ -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