Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions TransactionProcessor.Tests/HandlerTests/TransactionHandlersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.AspNetCore.Http;
using Moq;
using Newtonsoft.Json;
using Shared.General;
using Shouldly;
using SimpleResults;
using TransactionProcessor.BusinessLogic.Requests;
using TransactionProcessor.DataTransferObjects;
using TransactionProcessor.Handlers;
using TransactionProcessor.Models;
using Xunit;

namespace TransactionProcessor.Tests.HandlerTests
{
public class TransactionHandlersTests
{
[Fact]
public async Task PerformTransaction_LogonPayloadWithoutTypeMetadata_SendsLogonCommand()
{
Mock<IMediator> mediator = new Mock<IMediator>(MockBehavior.Strict);
LogonTransactionRequest request = new LogonTransactionRequest
{
DeviceIdentifier = "device-1",
TransactionDateTime = DateTime.SpecifyKind(new DateTime(2024, 1, 2, 3, 4, 5), DateTimeKind.Utc),
TransactionNumber = "000001",
TransactionType = "Logon"
};

mediator.Setup(m => m.Send(It.Is<TransactionCommands.ProcessLogonTransactionCommand>(command =>
command.EstateId == TestData.EstateId &&
command.MerchantId == TestData.MerchantId &&
command.DeviceIdentifier == request.DeviceIdentifier &&
command.TransactionNumber == request.TransactionNumber &&
command.TransactionType == request.TransactionType &&
command.TransactionDateTime.Kind == DateTimeKind.Unspecified &&
command.TransactionDateTime.Ticks == request.TransactionDateTime.Ticks),
It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Success(new ProcessLogonTransactionResponse
{
EstateId = TestData.EstateId,
MerchantId = TestData.MerchantId,
ResponseCode = "0000",
ResponseMessage = "SUCCESS",
TransactionId = Guid.NewGuid()
}));

IResult result = await TransactionHandlers.PerformTransaction(mediator.Object,
new DefaultHttpContext(),
CreateSerialisedMessage(request),
CancellationToken.None);

result.ShouldNotBeNull();
mediator.VerifyAll();
}

[Fact]
public async Task PerformTransaction_SalePayloadWithoutTypeMetadata_SendsSaleCommand()
{
Mock<IMediator> mediator = new Mock<IMediator>(MockBehavior.Strict);
SaleTransactionRequest request = new SaleTransactionRequest
{
AdditionalTransactionMetadata = new Dictionary<String, String> { { "amount", "12.34" } },
ContractId = Guid.NewGuid(),
CustomerEmailAddress = "customer@test.local",
DeviceIdentifier = "device-1",
OperatorId = Guid.NewGuid(),
ProductId = Guid.NewGuid(),
TransactionDateTime = new DateTime(2024, 1, 2, 3, 4, 5),
TransactionNumber = "000002",
TransactionSource = 2,
TransactionType = "Sale"
};

mediator.Setup(m => m.Send(It.Is<TransactionCommands.ProcessSaleTransactionCommand>(command =>
command.EstateId == TestData.EstateId &&
command.MerchantId == TestData.MerchantId &&
command.DeviceIdentifier == request.DeviceIdentifier &&
command.TransactionNumber == request.TransactionNumber &&
command.TransactionType == request.TransactionType &&
command.OperatorId == request.OperatorId &&
command.CustomerEmailAddress == request.CustomerEmailAddress &&
command.ContractId == request.ContractId &&
command.ProductId == request.ProductId &&
command.TransactionSource == request.TransactionSource &&
command.AdditionalTransactionMetadata["amount"] == "12.34"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Success(new ProcessSaleTransactionResponse
{
EstateId = TestData.EstateId,
MerchantId = TestData.MerchantId,
ResponseCode = "0000",
ResponseMessage = "SUCCESS",
TransactionId = Guid.NewGuid()
}));

IResult result = await TransactionHandlers.PerformTransaction(mediator.Object,
new DefaultHttpContext(),
CreateSerialisedMessage(request),
CancellationToken.None);

result.ShouldNotBeNull();
mediator.VerifyAll();
}

[Fact]
public async Task PerformTransaction_ReconciliationPayloadWithoutTypeMetadata_SendsReconciliationCommand()
{
Mock<IMediator> mediator = new Mock<IMediator>(MockBehavior.Strict);
ReconciliationRequest request = new ReconciliationRequest
{
DeviceIdentifier = "device-1",
OperatorTotals = new List<OperatorTotalRequest>(),
TransactionCount = 4,
TransactionDateTime = new DateTime(2024, 1, 2, 3, 4, 5),
TransactionValue = 42.50m
};

mediator.Setup(m => m.Send(It.Is<TransactionCommands.ProcessReconciliationCommand>(command =>
command.EstateId == TestData.EstateId &&
command.MerchantId == TestData.MerchantId &&
command.DeviceIdentifier == request.DeviceIdentifier &&
command.TransactionCount == request.TransactionCount &&
command.TransactionValue == request.TransactionValue),
It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Success(new ProcessReconciliationTransactionResponse
{
EstateId = TestData.EstateId,
MerchantId = TestData.MerchantId,
ResponseCode = "0000",
ResponseMessage = "SUCCESS",
TransactionId = Guid.NewGuid()
}));

IResult result = await TransactionHandlers.PerformTransaction(mediator.Object,
new DefaultHttpContext(),
CreateSerialisedMessage(request),
CancellationToken.None);

result.ShouldNotBeNull();
mediator.VerifyAll();
}

[Fact]
public async Task PerformTransaction_UnsupportedPayload_ReturnsBadRequest()
{
Mock<IMediator> mediator = new Mock<IMediator>(MockBehavior.Strict);

IResult result = await TransactionHandlers.PerformTransaction(mediator.Object,
new DefaultHttpContext(),
CreateSerialisedMessage(new
{
device_identifier = "device-1",
transaction_type = "Unknown"
}),
CancellationToken.None);

IStatusCodeHttpResult statusCodeResult = result.ShouldBeAssignableTo<IStatusCodeHttpResult>();
statusCodeResult.StatusCode.ShouldBe(StatusCodes.Status400BadRequest);
mediator.Verify(m => m.Send(It.IsAny<object>(), It.IsAny<CancellationToken>()), Times.Never);
}

private static SerialisedMessage CreateSerialisedMessage(Object request)
{
return new SerialisedMessage
{
Metadata = new Dictionary<String, String>
{
{ MetadataContants.KeyNameEstateId, TestData.EstateId.ToString() },
{ MetadataContants.KeyNameMerchantId, TestData.MerchantId.ToString() }
},
SerialisedData = JsonConvert.SerializeObject(request)
};
}

private static class TestData
{
public static Guid EstateId => Guid.Parse("11111111-1111-1111-1111-111111111111");
public static Guid MerchantId => Guid.Parse("22222222-2222-2222-2222-222222222222");
}
}
}
104 changes: 100 additions & 4 deletions TransactionProcessor/Handlers/TransactionHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using MediatR;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SimpleResults;
using Shared.Results.Web;
using Shared.General;
Expand All @@ -25,8 +26,13 @@ public static async Task<IResult> PerformTransaction(IMediator mediator, HttpCon
Guid estateId = Guid.Parse(transactionRequest.Metadata[MetadataContants.KeyNameEstateId]);
Guid merchantId = Guid.Parse(transactionRequest.Metadata[MetadataContants.KeyNameMerchantId]);

DataTransferObject dto = JsonConvert.DeserializeObject<DataTransferObject>(transactionRequest.SerialisedData,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });
Result<DataTransferObject> deserialiseResult = DeserializeTransactionRequest(transactionRequest.SerialisedData);
if (deserialiseResult.IsFailed || deserialiseResult.Data == null)
{
return ResponseFactory.FromResult(Result.Invalid(deserialiseResult.Message));
}

DataTransferObject dto = deserialiseResult.Data;

dto.MerchantId = merchantId;
dto.EstateId = estateId;
Expand All @@ -40,12 +46,102 @@ public static async Task<IResult> PerformTransaction(IMediator mediator, HttpCon
LogonTransactionRequest ltr => await ProcessSpecificMessage(mediator, ltr, transactionReceivedDateTime, cancellationToken),
SaleTransactionRequest str => await ProcessSpecificMessage(mediator, str, transactionReceivedDateTime, cancellationToken),
ReconciliationRequest rr => await ProcessSpecificMessage(mediator, rr, cancellationToken),
_ => Result.Invalid($"DTO Type {dto.GetType().Name} not supported)")
_ => Result.Invalid($"DTO Type {dto.GetType().Name} not supported")
};

return ResponseFactory.FromResult(transactionResult, message => message);
}

private static Result<DataTransferObject> DeserializeTransactionRequest(String serialisedData)
{
try {
JObject jsonObject = JObject.Parse(serialisedData);

if (IsReconciliationRequest(jsonObject)) {
return DeserializeKnownType<ReconciliationRequest>(jsonObject);
}

if (IsSaleRequest(jsonObject)) {
return DeserializeKnownType<SaleTransactionRequest>(jsonObject);
}

if (IsLogonRequest(jsonObject)) {
return DeserializeKnownType<LogonTransactionRequest>(jsonObject);
}

return Result.Invalid("DTO Type could not be determined");
}
catch (JsonException ex) {
return Result.Invalid($"Invalid transaction request payload: {ex.Message}");
}
}

private static Result<DataTransferObject> DeserializeKnownType<T>(JObject jsonObject) where T : DataTransferObject
{
try
{
JsonSerializer serializer = JsonSerializer.Create(new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.None,
MetadataPropertyHandling = MetadataPropertyHandling.Ignore
});

T dto = jsonObject.ToObject<T>(serializer);

if (dto == null)
{
return Result.Invalid($"Failed to deserialize transaction request as {typeof(T).Name}: deserialized payload was null");
}

return Result.Success<DataTransferObject>(dto);
}
catch (JsonException ex)
{
return Result.Invalid($"Failed to deserialize transaction request as {typeof(T).Name}: {ex.Message}");
}
}

private static Boolean IsLogonRequest(JObject jsonObject) {
if (TryGetTransactionType(jsonObject, out string transactionType)) {
return String.Equals(transactionType, "Logon", StringComparison.OrdinalIgnoreCase);
}

return false;
}

private static Boolean IsSaleRequest(JObject jsonObject) {
if (TryGetTransactionType(jsonObject, out string transactionType)) {
return String.Equals(transactionType, "Sale", StringComparison.OrdinalIgnoreCase);
}

return false;
}

private static Boolean IsReconciliationRequest(JObject jsonObject) {
if (TryGetTransactionType(jsonObject, out string _)) {
return false;
}

return true;
}

private static bool TryGetTransactionType(JObject jsonObject,
out string transactionType) {
transactionType = null;

if (!HasProperty(jsonObject, "transaction_type"))
return false;

transactionType = jsonObject.GetValue("transaction_type", StringComparison.OrdinalIgnoreCase)?.Value<string>();

return transactionType != null;
}

private static Boolean HasProperty(JObject jsonObject,
String propertyName) {
return jsonObject.GetValue(propertyName, StringComparison.OrdinalIgnoreCase) != null;
}

public static async Task<IResult> ResendTransactionReceipt(IMediator mediator, HttpContext ctx, Guid estateId, Guid transactionId, CancellationToken cancellationToken)
{
TransactionCommands.ResendTransactionReceiptCommand command = new(transactionId, estateId);
Expand Down Expand Up @@ -125,4 +221,4 @@ private static async Task<Result<SerialisedMessage>> ProcessSpecificMessage(IMed
return ModelFactory.ConvertFrom(result.Data);
}
}
}
}
Loading