From acb6aac4de4027aad38ab1774c2d8ef9c7712bb7 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Mon, 26 Jan 2026 10:03:33 +0000 Subject: [PATCH] Add device swap feature for merchants and refactor commands Introduce "Swap Device" UI for merchants, allowing users to swap assigned devices with validation and error handling. Refactor device management to use new AddMerchantDeviceCommand and SwapMerchantDeviceCommand under MerchantCommands. Update API client, request handlers, and test mediator to support new commands. Remove legacy device command usage and improve device list UI. --- .../Components/Pages/Merchants/Edit.razor | 39 ++++++++- .../Components/Pages/Merchants/Edit.razor.cs | 87 +++++++++++++++---- .../Client/MerchantMethods.cs | 32 +++++++ .../RequestHandlers/DateRequestHandler.cs | 12 +-- .../Requests/Requests.cs | 6 +- .../Services/TestMediatorService.cs | 4 +- 6 files changed, 149 insertions(+), 31 deletions(-) diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor index 87e1f347..b853143c 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor @@ -323,7 +323,7 @@ @if (assignedDevices == null || !assignedDevices.Any()) {

No devices assigned

} - else { + @* else {
@foreach (var device in assignedDevices) {
@@ -334,7 +334,42 @@
}
- } + } *@ + else { +
+ @foreach (var device in assignedDevices) { +
+
+ @device.DeviceIdentifier +
+ + @if (selectedDeviceToSwap == device.DeviceIdentifier) { +
+ + + +
+ } + else { +
+ +
+ } +
+ + @if (selectedDeviceToSwap == device.DeviceIdentifier && !string.IsNullOrEmpty(swapDeviceError)) + { +

@swapDeviceError

+ } + } +
+ } } diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor.cs index fd8ebc24..5dbd7ed6 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor.cs +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Edit.razor.cs @@ -2,7 +2,9 @@ using EstateManagementUI.BlazorServer.Factories; using EstateManagementUI.BlazorServer.Models; using EstateManagementUI.BlazorServer.Permissions; +using EstateManagementUI.BusinessLogic.BackendAPI.DataTransferObjects; using EstateManagementUI.BusinessLogic.Requests; +using MediatR; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Shared.Results; @@ -490,21 +492,12 @@ private async Task AddDeviceToMerchant() try { var correlationId = new CorrelationId(Guid.NewGuid()); - var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); - var accessToken = "stubbed-token"; - - var command = new Commands.AddMerchantDeviceCommand( - correlationId, - accessToken, - estateId, - MerchantId, - deviceIdentifier - ); + var estateId = await this.GetEstateId(); + IRequest command = new MerchantCommands.AddMerchantDeviceCommand(correlationId, estateId, MerchantId, deviceIdentifier); var result = await Mediator.Send(command); - if (result.IsSuccess) - { + if (result.IsSuccess) { successMessage = "Device added successfully"; assignedDevices.Add(new MerchantDeviceModel() { DeviceIdentifier = this.deviceIdentifier @@ -523,15 +516,71 @@ private async Task AddDeviceToMerchant() } } - private void RemoveDevice(string device) - { - ClearMessages(); - var d = this.assignedDevices.Single(d => d.DeviceIdentifier == device); - assignedDevices.Remove(d); - successMessage = "Device removed successfully"; + // inside partial class Edit (add the following fields and methods) + + // Swap device UI state + private string? selectedDeviceToSwap; + private string? swapDeviceIdentifier; + private string? swapDeviceError; + + private void StartSwapDevice(string device) + { + ClearMessages(); + selectedDeviceToSwap = device; + swapDeviceIdentifier = string.Empty; + swapDeviceError = null; + } + + private void CancelSwapDevice() + { + swapDeviceIdentifier = null; + swapDeviceError = null; + selectedDeviceToSwap = null; + } + + private async Task SwapDeviceConfirm(string originalDevice) + { + ClearMessages(); + + var newId = swapDeviceIdentifier?.Trim(); + + // Validation + if (string.IsNullOrWhiteSpace(newId)) { + swapDeviceError = "New device identifier is required."; + return; } - private void ClearMessages() + // Case-insensitive comparison for equality/duplicates + if (string.Equals(originalDevice?.Trim(), newId, StringComparison.OrdinalIgnoreCase)) { + swapDeviceError = "New device identifier cannot be the same as the current device."; + return; + } + + if (assignedDevices.Any(d => string.Equals(d.DeviceIdentifier.Trim(), newId, StringComparison.OrdinalIgnoreCase))) { + swapDeviceError = "The specified device identifier is already assigned."; + return; + } + + var correlationId = new CorrelationId(Guid.NewGuid()); + var estateId = await this.GetEstateId(); + var command = new MerchantCommands.SwapMerchantDeviceCommand(correlationId, estateId, this.MerchantId, this.assignedDevices.Single().DeviceIdentifier, newId); + + var result = await Mediator.Send(command); + + if (result.IsSuccess) { + successMessage = $"Device {originalDevice} swapped for {newId}."; + } + else { + swapDeviceError = "Original device not found."; + } + + // Reset swap UI + CancelSwapDevice(); + StateHasChanged(); + } + + + private void ClearMessages() { errorMessage = null; successMessage = null; diff --git a/EstateManagmentUI.BusinessLogic/Client/MerchantMethods.cs b/EstateManagmentUI.BusinessLogic/Client/MerchantMethods.cs index 7d1ba585..5d3965e4 100644 --- a/EstateManagmentUI.BusinessLogic/Client/MerchantMethods.cs +++ b/EstateManagmentUI.BusinessLogic/Client/MerchantMethods.cs @@ -26,6 +26,8 @@ public partial interface IApiClient Task AddOperatorToMerchant(MerchantCommands.AddOperatorToMerchantCommand request, CancellationToken cancellationToken); Task RemoveContractFromMerchant(MerchantCommands.RemoveContractFromMerchantCommand request, CancellationToken cancellationToken); Task AddContractToMerchant(MerchantCommands.AssignContractToMerchantCommand request, CancellationToken cancellationToken); + Task AddDeviceToMerchant(MerchantCommands.AddMerchantDeviceCommand request, CancellationToken cancellationToken); + Task SwapMerchantDevice(MerchantCommands.SwapMerchantDeviceCommand request, CancellationToken cancellationToken); } public partial class ApiClient : IApiClient { @@ -74,6 +76,36 @@ public async Task AddContractToMerchant(MerchantCommands.AssignContractT return Result.Success(); } + public async Task AddDeviceToMerchant(MerchantCommands.AddMerchantDeviceCommand request, + CancellationToken cancellationToken) { + var token = await this.GetToken(cancellationToken); + if (token.IsFailed) + return ResultHelpers.CreateFailure(token); + + AddMerchantDeviceRequest apiRequest = new() { DeviceIdentifier = request.DeviceIdentifier}; + + var apiResult = await this.TransactionProcessorClient.AddDeviceToMerchant(token.Data, request.EstateId, request.MerchantId, apiRequest, cancellationToken); + if (apiResult.IsFailed) + return ResultHelpers.CreateFailure(apiResult); + + return Result.Success(); + } + + public async Task SwapMerchantDevice(MerchantCommands.SwapMerchantDeviceCommand request, + CancellationToken cancellationToken) { + var token = await this.GetToken(cancellationToken); + if (token.IsFailed) + return ResultHelpers.CreateFailure(token); + + SwapMerchantDeviceRequest apiRequest = new() { NewDeviceIdentifier = request.NewDevice}; + + var apiResult = await this.TransactionProcessorClient.SwapDeviceForMerchant(token.Data, request.EstateId, request.MerchantId, request.OldDevice,apiRequest, cancellationToken); + if (apiResult.IsFailed) + return ResultHelpers.CreateFailure(apiResult); + + return Result.Success(); + } + public async Task RemoveOperatorFromMerchant(MerchantCommands.RemoveOperatorFromMerchantCommand request, CancellationToken cancellationToken) { var token = await this.GetToken(cancellationToken); diff --git a/EstateManagmentUI.BusinessLogic/RequestHandlers/DateRequestHandler.cs b/EstateManagmentUI.BusinessLogic/RequestHandlers/DateRequestHandler.cs index 33a19ba7..2c806642 100644 --- a/EstateManagmentUI.BusinessLogic/RequestHandlers/DateRequestHandler.cs +++ b/EstateManagmentUI.BusinessLogic/RequestHandlers/DateRequestHandler.cs @@ -54,13 +54,13 @@ public async Task>> Handle(EstateQueries.GetAssignedO public class MerchantRequestHandler : IRequestHandler>>, IRequestHandler>, - IRequestHandler, + IRequestHandler, IRequestHandler, IRequestHandler, IRequestHandler, IRequestHandler, IRequestHandler, - IRequestHandler, + IRequestHandler, IRequestHandler, IRequestHandler, IRequestHandler>>, @@ -82,9 +82,9 @@ public async Task>> Handle(MerchantQueries.GetMer return await this.ApiClient.GetMerchants(request, cancellationToken); } - public async Task Handle(Commands.AddMerchantDeviceCommand request, + public async Task Handle(MerchantCommands.AddMerchantDeviceCommand request, CancellationToken cancellationToken) { - return Result.Success(); + return await this.ApiClient.AddDeviceToMerchant(request, cancellationToken); } public async Task Handle(MerchantCommands.AddOperatorToMerchantCommand request, @@ -112,9 +112,9 @@ public async Task Handle(MerchantCommands.RemoveOperatorFromMerchantComm return await this.ApiClient.RemoveOperatorFromMerchant(request, cancellationToken); } - public async Task Handle(Commands.SwapMerchantDeviceCommand request, + public async Task Handle(MerchantCommands.SwapMerchantDeviceCommand request, CancellationToken cancellationToken) { - return Result.Success(); + return await this.ApiClient.SwapMerchantDevice(request, cancellationToken); } diff --git a/EstateManagmentUI.BusinessLogic/Requests/Requests.cs b/EstateManagmentUI.BusinessLogic/Requests/Requests.cs index 5d397a67..d0862972 100644 --- a/EstateManagmentUI.BusinessLogic/Requests/Requests.cs +++ b/EstateManagmentUI.BusinessLogic/Requests/Requests.cs @@ -82,18 +82,20 @@ public record RemoveOperatorFromMerchantCommand(CorrelationId CorrelationId, Gui public record AddOperatorToMerchantCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, Guid OperatorId, string? MerchantNumber, string? TerminalNumber) : IRequest; public record RemoveContractFromMerchantCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, Guid ContractId) : IRequest; public record AssignContractToMerchantCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, Guid ContractId) : IRequest; + public record AddMerchantDeviceCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, string DeviceIdentifier) : IRequest; + public record SwapMerchantDeviceCommand(CorrelationId CorrelationId, Guid EstateId, Guid MerchantId, string OldDevice, string NewDevice) : IRequest; } public static class Commands { - public record AddMerchantDeviceCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid MerchantId, string DeviceIdentifier) : IRequest; + public record CreateContractCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, string Description, Guid OperatorId) : IRequest; public record CreateMerchantCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, string Name, string ContactName, string ContactEmail) : IRequest; public record CreateMerchantUserCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid MerchantId, string EmailAddress, string Password) : IRequest; public record CreateOperatorCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, string Name, bool RequireCustomMerchantNumber, bool RequireCustomTerminalNumber) : IRequest; public record MakeMerchantDepositCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid MerchantId, decimal Amount, DateTime Date, string Reference) : IRequest; - public record SwapMerchantDeviceCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid MerchantId, string OldDevice, string NewDevice) : IRequest; + public record UpdateOperatorCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid OperatorId, string Name, bool RequireCustomMerchantNumber, bool RequireCustomTerminalNumber) : IRequest; public record AddProductToContractCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid ContractId, string ProductName, string DisplayText, decimal? Value) : IRequest; public record AddTransactionFeeForProductToContractCommand(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid ContractId, Guid ProductId, string Description, decimal Value) : IRequest; diff --git a/EstateManagmentUI.BusinessLogic/Services/TestMediatorService.cs b/EstateManagmentUI.BusinessLogic/Services/TestMediatorService.cs index 744d2be5..33217582 100644 --- a/EstateManagmentUI.BusinessLogic/Services/TestMediatorService.cs +++ b/EstateManagmentUI.BusinessLogic/Services/TestMediatorService.cs @@ -80,8 +80,8 @@ public Task Send(IRequest request, Cancellation MerchantCommands.RemoveOperatorFromMerchantCommand cmd => Task.FromResult((TResponse)(object)this.ExecuteRemoveOperatorFromMerchant(cmd)), EstateCommands.AddOperatorToEstateCommand cmd => Task.FromResult((TResponse)(object)this.ExecuteAddOperatorToEstate(cmd)), EstateCommands.RemoveOperatorFromEstateCommand cmd => Task.FromResult((TResponse)(object)this.ExecuteRemoveOperatorFromEstate(cmd)), - Commands.AddMerchantDeviceCommand => Task.FromResult((TResponse)(object)Result.Success()), - Commands.SwapMerchantDeviceCommand => Task.FromResult((TResponse)(object)Result.Success()), + MerchantCommands.AddMerchantDeviceCommand => Task.FromResult((TResponse)(object)Result.Success()), + MerchantCommands.SwapMerchantDeviceCommand => Task.FromResult((TResponse)(object)Result.Success()), Commands.CreateMerchantUserCommand => Task.FromResult((TResponse)(object)Result.Success()), Commands.MakeMerchantDepositCommand cmd => Task.FromResult((TResponse)(object)this.ExecuteMakeMerchantDeposit(cmd)),