Skip to content

Commit

Permalink
Delete customer functionality (#81)
Browse files Browse the repository at this point in the history
* Initial draft

* Some more initial changes

* Delete user funnctionality

* Prepend comments to source files

* Fixed tests

* Removed hardcoded code for testing

---------

Co-authored-by: GitHub Action <[email protected]>
  • Loading branch information
nagarwal4 and actions-user authored Apr 19, 2024
1 parent 142fdcb commit f4719a8
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 14 deletions.
4 changes: 2 additions & 2 deletions backend/core/src/Core.API/Core.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Quantoz.Nexus.Sdk.Shared" Version="1.7.1" />
<PackageReference Include="Quantoz.Nexus.Sdk.Token" Version="1.7.1" />
<PackageReference Include="Quantoz.Nexus.Sdk.Shared" Version="1.9.0" />
<PackageReference Include="Quantoz.Nexus.Sdk.Token" Version="1.9.0" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.8.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.8.1" />
<PackageReference Include="ReHackt.Extensions.Options.Validation" Version="8.0.2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using Core.Domain.Abstractions;
using Core.Domain.Repositories;
using Core.Infrastructure.AzureB2CGraphService;
using Core.Infrastructure.Compliance;
using Core.Infrastructure.Compliance.IPLocator;
using Core.Infrastructure.Compliance.Sanctionlist;
Expand All @@ -14,6 +15,7 @@
using Core.Infrastructure.Nexus.Repositories;
using Core.Infrastructure.Nexus.SigningService;
using Microsoft.Extensions.Options;
using Microsoft.Graph;
using Nexus.Sdk.Shared.Http;
using Nexus.Sdk.Token.Extensions;
using Quartz;
Expand All @@ -25,6 +27,7 @@ public static class InfrastructureInjection
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services
.AddB2CGraphService(configuration)
.AddNexus(configuration)
.AddIPLocator(configuration)
.AddSanctionlist(configuration)
Expand Down Expand Up @@ -83,6 +86,23 @@ private static IServiceCollection AddIPLocator(this IServiceCollection services,
return services;
}

public static IServiceCollection AddB2CGraphService(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<B2CServiceOptions>()
.Bind(configuration.GetSection("B2CServiceOptions"))
.ValidateDataAnnotationsRecursively()
.ValidateOnStart();

services.AddSingleton(sp => sp.GetRequiredService<IOptions<B2CServiceOptions>>().Value);

services.AddHttpClient<IB2CGraphService, B2CGraphService>();

services.AddScoped<IB2CGraphService, B2CGraphService>();
services.AddScoped<GraphServiceClient>();

return services;
}

private static IServiceCollection AddSanctionlist(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<SanctionlistOptions>()
Expand Down
5 changes: 5 additions & 0 deletions backend/core/src/Core.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"Audience": ""
}
],
"B2CServiceOptions": {
"ClientId": "",
"ClientSecret": "",
"Tenant": ""
},
"NexusOptions": {
"ApiUrl": "",
"PaymentMethodOptions": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed
// under the Apache License, Version 2.0. See the NOTICE file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

using Core.Domain.Repositories;
using MediatR;

namespace Core.Application.Commands.CustomerCommands
{
public class DeleteCustomerCommand : IRequest
{
public string CustomerCode { get; set; }

public string IP { get; set; }

public DeleteCustomerCommand(string customerCode, string iP)
{
CustomerCode = customerCode;
IP = iP;
}
}

public class DeleteCustomerCommandHandler : IRequestHandler<DeleteCustomerCommand>
{
private readonly ICustomerRepository _customerRepository;

public DeleteCustomerCommandHandler(
ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}

public async Task Handle(DeleteCustomerCommand request, CancellationToken cancellationToken)
{
var customer = await _customerRepository.GetAsync(request.CustomerCode, cancellationToken);

await _customerRepository.DeleteAsync(customer, request.IP);
}
}
}
3 changes: 2 additions & 1 deletion backend/core/src/Core.Application/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public enum ApplicationErrorCode
{
InvalidStatusError,
InvalidPropertyError,
ExistingPropertyError
ExistingPropertyError,
NotFoundError
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ public interface ICustomerRepository
public Task<IEnumerable<CustomerLimit>> GetLimitsAsync(string customerCode, CancellationToken cancellationToken = default);

public Task UpdateAsync(Customer customer, CancellationToken cancellationToken = default);

public Task DeleteAsync(Customer customer, string? ip = null, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed
// under the Apache License, Version 2.0. See the NOTICE file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

using Azure.Identity;
using Core.Domain.Exceptions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Graph;

namespace Core.Infrastructure.AzureB2CGraphService
{
public class B2CGraphService : IB2CGraphService
{
private readonly GraphServiceClient _graphServiceClient;
private readonly ILogger<B2CGraphService> _logger;

public B2CGraphService(
GraphServiceClient graphServiceClient,
IOptions<B2CServiceOptions> options,
ILogger<B2CGraphService> logger)
{
_graphServiceClient = graphServiceClient;
_logger = logger;

var clientId = options.Value.ClientId;
var clientSecret = options.Value.ClientSecret;
var tenant = options.Value.Tenant;

var clientSecretCredential = new ClientSecretCredential(tenant, clientId, clientSecret);

var scopes = new[] { "https://graph.microsoft.com/.default" };

_graphServiceClient = new GraphServiceClient(clientSecretCredential, scopes);
}

public async Task DeleteUserAsync(string customerCode)
{
_logger.LogInformation("Attempting to delete user {CustomerId} from B2C.", customerCode);

try
{
var user = await _graphServiceClient.Users[customerCode].GetAsync();

if (user != null)
{
await _graphServiceClient.Users[customerCode].DeleteAsync();
_logger.LogInformation("User {CustomerId} deleted successfully.", customerCode);
}
else
{
_logger.LogInformation("User {CustomerId} not found.", customerCode);
}
}
catch (ServiceException ex) when (ex.IsMatch(GraphErrorCode.Request_ResourceNotFound.ToString()))
{
_logger.LogInformation("User {CustomerId} not found.", customerCode);
throw new CustomErrorsException("B2CGraphService", customerCode, "User not found.");
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred while deleting user {CustomerId}.", customerCode);
throw new CustomErrorsException("B2CGraphService", customerCode, "An unexpected error occurred while deleting user.");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed
// under the Apache License, Version 2.0. See the NOTICE file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

using System.ComponentModel.DataAnnotations;

namespace Core.Infrastructure.AzureB2CGraphService
{
public class B2CServiceOptions
{
[Required]
public required string ClientId { get; set; }

[Required]
public required string ClientSecret { get; set; }

[Required]
public required string Tenant { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed
// under the Apache License, Version 2.0. See the NOTICE file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

using Core.Domain.Entities.CustomerAggregate;

namespace Core.Infrastructure.AzureB2CGraphService
{
public interface IB2CGraphService
{
public Task DeleteUserAsync(string customerCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.10.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.2" />
<PackageReference Include="Microsoft.Graph" Version="5.48.0" />
<PackageReference Include="Otp.NET" Version="1.3.0" />
<PackageReference Include="Quantoz.Nexus.Sdk.Token" Version="1.7.1" />
<PackageReference Include="Quantoz.Nexus.Sdk.Token" Version="1.9.0" />
<PackageReference Include="Quartz" Version="3.8.1" />
<PackageReference Include="SendGrid" Version="9.29.2" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
Expand Down
3 changes: 2 additions & 1 deletion backend/core/src/Core.Infrastructure/Nexus/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public enum NexusErrorCodes
TrustlevelNotFoundError = 4,
InvalidStatus = 4,
InvalidProperty = 5,
TransactionNotFoundError = 6
TransactionNotFoundError = 6,
NonZeroAccountBalance = 7,
}

public enum MailStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,33 @@
using Core.Domain.Entities.CustomerAggregate;
using Core.Domain.Exceptions;
using Core.Domain.Repositories;
using Core.Infrastructure.AzureB2CGraphService;
using Core.Infrastructure.Nexus.SigningService;
using Nexus.Sdk.Shared.Requests;
using Nexus.Sdk.Token;
using Nexus.Sdk.Token.Requests;
using Nexus.Sdk.Token.Responses;
using Account = Core.Domain.Entities.AccountAggregate.Account;

namespace Core.Infrastructure.Nexus.Repositories
{
public class NexusCustomerRepository : ICustomerRepository
{
private readonly ITokenServer _tokenServer;
private readonly TokenOptions _tokenSettings;

public NexusCustomerRepository(ITokenServer tokenServer, TokenOptions tokenSettings)
private readonly ISigningService _signingService;
private readonly IB2CGraphService _b2cGraphService;

public NexusCustomerRepository(
ITokenServer tokenServer,
TokenOptions tokenSettings,
ISigningService signingService,
IB2CGraphService b2CGraphService)
{
_tokenServer = tokenServer;
_tokenSettings = tokenSettings;
_signingService = signingService;
_b2cGraphService = b2CGraphService;
}

public async Task CreateAsync(Customer customer, string? ip = null, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -144,6 +156,104 @@ public async Task<IEnumerable<CustomerLimit>> GetLimitsAsync(string customerCode
return limits;
}

public async Task DeleteAsync(Customer customer, string? ip = null, CancellationToken cancellationToken = default)
{
var customerCodeExists = await _tokenServer.Customers.Exists(customer.CustomerCode);

if (!customerCodeExists)
{
throw new CustomErrorsException(NexusErrorCodes.CustomerNotFoundError.ToString(), customer.CustomerCode, Constants.NexusErrorMessages.CustomerNotFound);
}

// Check if the customer status is ACTIVE
if (customer.Status != CustomerStatus.ACTIVE.ToString())
{
throw new CustomErrorsException(NexusErrorCodes.InvalidStatus.ToString(), customer.Status.ToString(), "Invalid customer status");
}

// Get Customer accounts
var accounts = await _tokenServer.Accounts.Get(
new Dictionary<string, string>
{
{ "CustomerCode", customer.CustomerCode },
{ "Status", "ACTIVE" }
});

// Check if the customer has any accounts and balance
if (accounts.Records.Any())
{
foreach (var acc in accounts.Records)
{
Account account = new()
{
AccountCode = acc.AccountCode,
CustomerCode = acc.CustomerCode,
PublicKey = acc.PublicKey
};

// Get the account balance
var response = await _tokenServer.Accounts.GetBalances(account.AccountCode);

var accountBalances = response.Balances;

if (response.Balances.Any(balance => balance.Amount > 0))
{
throw new CustomErrorsException(NexusErrorCodes.NonZeroAccountBalance.ToString(), account.AccountCode, "Customer cannot be deleted due to non-zero balance");
}

if (accountBalances.Any())
{
// Get the token codes to be disabled
var tokensToBeDisabled = accountBalances.Select(b => b.TokenCode).ToArray();

await RemoveTrustlines(customer, account, tokensToBeDisabled);
}
}
}

var deleteRequest = new DeleteCustomerRequest
{
CustomerCode = customer.CustomerCode
};

await _tokenServer.Customers.Delete(deleteRequest, ip);

// delete user from azure b2c
await _b2cGraphService.DeleteUserAsync(customer.CustomerCode);
}

private async Task RemoveTrustlines(Customer customer, Account account, string[]? tokensToBeDisabled = null)
{
var updateAccount = new UpdateTokenAccountRequest
{
Settings = new UpdateTokenAccountSettings
{
AllowedTokens = new AllowedTokens
{
DisableTokens = tokensToBeDisabled
}
}
};

var signableResponse = await _tokenServer.Accounts.Update(customer.CustomerCode, account.AccountCode, updateAccount);

switch (_tokenSettings.Blockchain)
{
case Blockchain.STELLAR:
{
var submitRequest = await _signingService.SignStellarTransactionEnvelopeAsync(account.PublicKey, signableResponse);
await _tokenServer.Submit.OnStellarAsync(submitRequest);
break;
}
case Blockchain.ALGORAND:
{
var submitRequest = await _signingService.SignAlgorandTransactionAsync(account.PublicKey, signableResponse);
await _tokenServer.Submit.OnAlgorandAsync(submitRequest);
break;
}
}
}

public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(true);
Expand Down
Loading

0 comments on commit f4719a8

Please sign in to comment.