Skip to content

Commit b0822c1

Browse files
Add resend receipt endpoint and command support
Added support for resending transaction receipts via a new API endpoint and command. Introduced DTOs for request and response, updated the application service and handler logic, and implemented validation. Extended unit and integration tests to cover the new functionality. Refactored integration test table variable names for clarity.
1 parent 3e38f32 commit b0822c1

13 files changed

Lines changed: 607 additions & 229 deletions

File tree

TransactionProcessorACL.BusinessLogic.Tests/MediatorTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace TransactionProcessorACL.BusinessLogic.Tests
1818
{
1919
using System.Threading;
2020
using Microsoft.Extensions.DependencyInjection;
21+
using TransactionProcessorACL.BusinessLogic.Requests;
2122
using TransactionProcessorACL.DataTransferObjects.Requests;
2223
using Models;
2324
using Services;
@@ -40,6 +41,7 @@ public MediatorTests()
4041
this.Requests.Add(TestData.GetMerchantDailyPerformanceSummaryQuery);
4142
this.Requests.Add(TestData.GetMerchantTransactionMixSummaryQuery);
4243
this.Requests.Add(TestData.GetRecentActivityReceiptSearchQuery);
44+
this.Requests.Add(new TransactionCommands.ResendReceiptCommand(TestData.EstateId, TestData.MerchantId, "RCPT-0001", "recipient@example.com"));
4345
}
4446

4547
[Fact]
@@ -185,5 +187,14 @@ public async Task<Result<RecentActivityReceiptSearchResponse>> GetRecentActivity
185187
{
186188
return Result.Success(new RecentActivityReceiptSearchResponse());
187189
}
190+
191+
public async Task<Result<ResendReceiptResponse>> ResendReceipt(Guid estateId,
192+
Guid merchantId,
193+
String reference,
194+
String recipientEmailAddress,
195+
CancellationToken cancellationToken)
196+
{
197+
return Result.Success(new ResendReceiptResponse { Success = true, Message = "Receipt resend requested." });
198+
}
188199
}
189200
}

TransactionProcessorACL.BusinessLogic.Tests/RequestHandlerTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,33 @@ public async Task ReportingRequestHandler_GetRecentActivityReceiptSearchQuery_Ha
285285
result.Data.ShouldNotBeNull();
286286
}
287287

288+
[Fact]
289+
public async Task TransactionRequestHandler_ResendReceiptCommand_Handle_RequestIsHandled()
290+
{
291+
Mock<ITransactionProcessorACLApplicationService> applicationService = new Mock<ITransactionProcessorACLApplicationService>();
292+
applicationService
293+
.Setup(a => a.ResendReceipt(It.IsAny<Guid>(),
294+
It.IsAny<Guid>(),
295+
It.IsAny<String>(),
296+
It.IsAny<String>(),
297+
It.IsAny<CancellationToken>()))
298+
.ReturnsAsync(new ResendReceiptResponse { Success = true, Message = "Receipt resend requested." });
299+
300+
TransactionRequestHandler requestHandler = new TransactionRequestHandler(applicationService.Object);
301+
302+
TransactionCommands.ResendReceiptCommand command = new(
303+
TestData.EstateId,
304+
TestData.MerchantId,
305+
"RCPT-0001",
306+
"recipient@example.com");
307+
308+
Result<ResendReceiptResponse> result = await requestHandler.Handle(command, CancellationToken.None);
309+
310+
result.IsSuccess.ShouldBeTrue();
311+
result.Data.ShouldNotBeNull();
312+
result.Data.Success.ShouldBeTrue();
313+
}
314+
288315
#endregion
289316
}
290317
}

TransactionProcessorACL.BusinessLogic/RequestHandlers/ProcessLogonTransactionRequestHandler.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ namespace TransactionProcessorACL.BusinessLogic.RequestHandlers
1616
/// <seealso cref="ProcessLogonTransactionResponse" />
1717
public class TransactionRequestHandler : IRequestHandler<TransactionCommands.ProcessLogonTransactionCommand, Result<ProcessLogonTransactionResponse>>,
1818
IRequestHandler<TransactionCommands.ProcessReconciliationCommand, Result<ProcessReconciliationResponse>>,
19-
IRequestHandler<TransactionCommands.ProcessSaleTransactionCommand, Result<ProcessSaleTransactionResponse>>
19+
IRequestHandler<TransactionCommands.ProcessSaleTransactionCommand, Result<ProcessSaleTransactionResponse>>,
20+
IRequestHandler<TransactionCommands.ResendReceiptCommand, Result<ResendReceiptResponse>>
2021
{
2122
#region Fields
2223

@@ -62,7 +63,7 @@ public async Task<Result<ProcessReconciliationResponse>> Handle(TransactionComma
6263
}
6364

6465
public async Task<Result<ProcessSaleTransactionResponse>> Handle(TransactionCommands.ProcessSaleTransactionCommand command,
65-
CancellationToken cancellationToken)
66+
CancellationToken cancellationToken)
6667
{
6768
return await this.ApplicationService.ProcessSaleTransaction((command.EstateId, command.MerchantId),
6869
command.TransactionDateTime,
@@ -74,6 +75,16 @@ public async Task<Result<ProcessSaleTransactionResponse>> Handle(TransactionComm
7475
cancellationToken);
7576
}
7677

78+
public async Task<Result<ResendReceiptResponse>> Handle(TransactionCommands.ResendReceiptCommand command,
79+
CancellationToken cancellationToken)
80+
{
81+
return await this.ApplicationService.ResendReceipt(command.EstateId,
82+
command.MerchantId,
83+
command.Reference,
84+
command.RecipientEmailAddress,
85+
cancellationToken);
86+
}
87+
7788
#endregion
7889
}
7990
}

TransactionProcessorACL.BusinessLogic/Requests/TransactionCommands.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,10 @@ public record ProcessReconciliationCommand(Guid EstateId,
3636
Int32 TransactionCount,
3737
Decimal TransactionValue)
3838
: IRequest<Result<ProcessReconciliationResponse>>;
39+
40+
public record ResendReceiptCommand(Guid EstateId,
41+
Guid MerchantId,
42+
String Reference,
43+
String RecipientEmailAddress)
44+
: IRequest<Result<ResendReceiptResponse>>;
3945
}

TransactionProcessorACL.BusinessLogic/Services/ITransactionProcessorACLApplicationService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,11 @@ Task<Result<MerchantTransactionMixSummaryResponse>> GetMerchantTransactionMixSum
7373
Task<Result<RecentActivityReceiptSearchResponse>> GetRecentActivityReceiptSearch(Guid estateId,
7474
RecentActivityReceiptSearchRequest request,
7575
CancellationToken cancellationToken);
76+
77+
Task<Result<ResendReceiptResponse>> ResendReceipt(Guid estateId,
78+
Guid merchantId,
79+
String reference,
80+
String recipientEmailAddress,
81+
CancellationToken cancellationToken);
7682
}
7783
}

TransactionProcessorACL.BusinessLogic/Services/TransactionProcessorACLApplicationService.cs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,200 @@ public async Task<Result<RecentActivityReceiptSearchResponse>> GetRecentActivity
492492
return response;
493493
}
494494

495+
public async Task<Result<ResendReceiptResponse>> ResendReceipt(Guid estateId,
496+
Guid merchantId,
497+
String reference,
498+
String recipientEmailAddress,
499+
CancellationToken cancellationToken)
500+
{
501+
Result<TokenResponse> accessTokenResult = await this.GetAccessToken(cancellationToken);
502+
if (accessTokenResult.IsFailed) {
503+
return ResultHelpers.CreateFailure(accessTokenResult);
504+
}
505+
506+
if (string.IsNullOrWhiteSpace(reference)) {
507+
return Result.Invalid("A transaction reference or receipt reference is required.");
508+
}
509+
510+
if (string.IsNullOrWhiteSpace(recipientEmailAddress)) {
511+
return Result.Invalid("Recipient email address is required.");
512+
}
513+
514+
try {
515+
_ = new System.Net.Mail.MailAddress(recipientEmailAddress);
516+
}
517+
catch (FormatException) {
518+
return Result.Invalid("Recipient email address is not valid.");
519+
}
520+
521+
Result<ResendReceiptResponse> resendResult = await this.InvokeResendReceiptAsync(accessTokenResult.Data.AccessToken,
522+
estateId,
523+
merchantId,
524+
reference,
525+
recipientEmailAddress,
526+
cancellationToken);
527+
528+
if (resendResult.IsFailed) {
529+
return resendResult;
530+
}
531+
532+
return resendResult;
533+
}
534+
535+
private async Task<Result<ResendReceiptResponse>> InvokeResendReceiptAsync(String accessToken,
536+
Guid estateId,
537+
Guid merchantId,
538+
String reference,
539+
String recipientEmailAddress,
540+
CancellationToken cancellationToken)
541+
{
542+
System.Reflection.MethodInfo? method = typeof(ITransactionProcessorClient)
543+
.GetMethods()
544+
.SingleOrDefault(m => string.Equals(m.Name, "ResendEmailReceipt", StringComparison.Ordinal));
545+
546+
if (method == null) {
547+
return Result.Failure("Receipt resend is not supported by the transaction client.");
548+
}
549+
550+
System.Reflection.ParameterInfo[] parameters = method.GetParameters();
551+
object?[] args = new object?[parameters.Length];
552+
bool estateAssigned = false;
553+
bool merchantAssigned = false;
554+
object? request = null;
555+
556+
for (int index = 0; index < parameters.Length; index++) {
557+
System.Reflection.ParameterInfo parameter = parameters[index];
558+
559+
if (parameter.ParameterType == typeof(String)) {
560+
args[index] = accessToken;
561+
continue;
562+
}
563+
564+
if (parameter.ParameterType == typeof(Guid)) {
565+
if (estateAssigned == false) {
566+
args[index] = estateId;
567+
estateAssigned = true;
568+
}
569+
else if (merchantAssigned == false) {
570+
args[index] = merchantId;
571+
merchantAssigned = true;
572+
}
573+
else {
574+
args[index] = Guid.Empty;
575+
}
576+
577+
continue;
578+
}
579+
580+
if (parameter.ParameterType == typeof(CancellationToken)) {
581+
args[index] = cancellationToken;
582+
continue;
583+
}
584+
585+
request ??= System.Activator.CreateInstance(parameter.ParameterType);
586+
if (request == null) {
587+
return Result.Failure($"Unable to create resend receipt request type '{parameter.ParameterType.FullName}'.");
588+
}
589+
590+
PopulateResendReceiptRequest(request, estateId, merchantId, reference, recipientEmailAddress);
591+
args[index] = request;
592+
}
593+
594+
object? invocationResult = method.Invoke(this.TransactionProcessorClient, args);
595+
if (invocationResult is not Task task) {
596+
return Result.Failure("Receipt resend call did not return a task.");
597+
}
598+
599+
await task.ConfigureAwait(false);
600+
601+
object? taskResult = task.GetType().GetProperty("Result")?.GetValue(task);
602+
if (taskResult == null) {
603+
return Result.Failure("Receipt resend call returned no result.");
604+
}
605+
606+
bool isFailed = (bool)(taskResult.GetType().GetProperty("IsFailed")?.GetValue(taskResult) ?? true);
607+
if (isFailed) {
608+
string? message = taskResult.GetType().GetProperty("Message")?.GetValue(taskResult)?.ToString();
609+
string? status = taskResult.GetType().GetProperty("Status")?.GetValue(taskResult)?.ToString();
610+
611+
if (string.Equals(status, "NotFound", StringComparison.OrdinalIgnoreCase)) {
612+
return Result.NotFound(message ?? "Receipt could not be found.");
613+
}
614+
615+
if (string.Equals(status, "Invalid", StringComparison.OrdinalIgnoreCase)) {
616+
return Result.Invalid(message ?? "Receipt resend request was invalid.");
617+
}
618+
619+
return Result.Failure(message ?? "Receipt resend failed.");
620+
}
621+
622+
object? data = taskResult.GetType().GetProperty("Data")?.GetValue(taskResult);
623+
return Result.Success(MapResendReceiptResponse(data, reference));
624+
}
625+
626+
private static void PopulateResendReceiptRequest(object request,
627+
Guid estateId,
628+
Guid merchantId,
629+
String reference,
630+
String recipientEmailAddress)
631+
{
632+
Type requestType = request.GetType();
633+
SetIfPresent(requestType, request, "EstateId", estateId);
634+
SetIfPresent(requestType, request, "MerchantId", merchantId);
635+
SetIfPresent(requestType, request, "Reference", reference);
636+
SetIfPresent(requestType, request, "ReceiptReference", reference);
637+
SetIfPresent(requestType, request, "TransactionReference", reference);
638+
SetIfPresent(requestType, request, "RecipientEmail", recipientEmailAddress);
639+
SetIfPresent(requestType, request, "RecipientEmailAddress", recipientEmailAddress);
640+
SetIfPresent(requestType, request, "EmailAddress", recipientEmailAddress);
641+
}
642+
643+
private static void SetIfPresent(Type requestType, object request, String propertyName, object value)
644+
{
645+
System.Reflection.PropertyInfo? property = requestType.GetProperty(propertyName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase);
646+
if (property == null || property.CanWrite == false) {
647+
return;
648+
}
649+
650+
property.SetValue(request, value);
651+
}
652+
653+
private static ResendReceiptResponse MapResendReceiptResponse(object? data, String reference)
654+
{
655+
ResendReceiptResponse response = new()
656+
{
657+
Success = true,
658+
Message = "Receipt resend requested.",
659+
Reference = reference
660+
};
661+
662+
if (data == null) {
663+
return response;
664+
}
665+
666+
Type responseType = data.GetType();
667+
response.Message = GetStringProperty(responseType, data, "Message") ?? response.Message;
668+
response.Reference = GetStringProperty(responseType, data, "Reference") ?? response.Reference;
669+
response.ReceiptReference = GetStringProperty(responseType, data, "ReceiptReference");
670+
response.TransactionReference = GetStringProperty(responseType, data, "TransactionReference");
671+
672+
if (string.IsNullOrWhiteSpace(response.Reference) && string.IsNullOrWhiteSpace(response.ReceiptReference) == false) {
673+
response.Reference = response.ReceiptReference;
674+
}
675+
676+
if (string.IsNullOrWhiteSpace(response.Reference) && string.IsNullOrWhiteSpace(response.TransactionReference) == false) {
677+
response.Reference = response.TransactionReference;
678+
}
679+
680+
return response;
681+
}
682+
683+
private static string? GetStringProperty(Type type, object instance, String propertyName)
684+
{
685+
System.Reflection.PropertyInfo? property = type.GetProperty(propertyName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.IgnoreCase);
686+
return property?.GetValue(instance)?.ToString();
687+
}
688+
495689
private static ProcessReconciliationResponse CreateProcessReconciliationResponse(ReconciliationResponse reconciliationResponse)
496690
{
497691
return new ProcessReconciliationResponse
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace TransactionProcessorACL.DataTransferObjects.Requests;
4+
5+
[ExcludeFromCodeCoverage]
6+
public class ResendReceiptRequestMessage
7+
{
8+
public string Reference { get; set; }
9+
10+
public string RecipientEmailAddress { get; set; }
11+
}

0 commit comments

Comments
 (0)