diff --git a/backend/core/src/Core.API/Core.API.csproj b/backend/core/src/Core.API/Core.API.csproj
index 3948f18..6944383 100644
--- a/backend/core/src/Core.API/Core.API.csproj
+++ b/backend/core/src/Core.API/Core.API.csproj
@@ -22,8 +22,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs b/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs
index 636cb3f..ce12971 100644
--- a/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs
+++ b/backend/core/src/Core.API/DependencyInjection/InfrastructureInjection.cs
@@ -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;
@@ -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;
@@ -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)
@@ -83,6 +86,23 @@ private static IServiceCollection AddIPLocator(this IServiceCollection services,
return services;
}
+ public static IServiceCollection AddB2CGraphService(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddOptions()
+ .Bind(configuration.GetSection("B2CServiceOptions"))
+ .ValidateDataAnnotationsRecursively()
+ .ValidateOnStart();
+
+ services.AddSingleton(sp => sp.GetRequiredService>().Value);
+
+ services.AddHttpClient();
+
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+
private static IServiceCollection AddSanctionlist(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions()
diff --git a/backend/core/src/Core.API/appsettings.json b/backend/core/src/Core.API/appsettings.json
index 35a653f..1d5318b 100644
--- a/backend/core/src/Core.API/appsettings.json
+++ b/backend/core/src/Core.API/appsettings.json
@@ -12,6 +12,11 @@
"Audience": ""
}
],
+ "B2CServiceOptions": {
+ "ClientId": "",
+ "ClientSecret": "",
+ "Tenant": ""
+ },
"NexusOptions": {
"ApiUrl": "",
"PaymentMethodOptions": {
diff --git a/backend/core/src/Core.Application/Commands/CustomerCommands/DeleteCustomerCommand.cs b/backend/core/src/Core.Application/Commands/CustomerCommands/DeleteCustomerCommand.cs
new file mode 100644
index 0000000..c8cb5d8
--- /dev/null
+++ b/backend/core/src/Core.Application/Commands/CustomerCommands/DeleteCustomerCommand.cs
@@ -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
+ {
+ 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);
+ }
+ }
+}
diff --git a/backend/core/src/Core.Application/Enums.cs b/backend/core/src/Core.Application/Enums.cs
index 2dfeb26..7cae80e 100644
--- a/backend/core/src/Core.Application/Enums.cs
+++ b/backend/core/src/Core.Application/Enums.cs
@@ -8,6 +8,7 @@ public enum ApplicationErrorCode
{
InvalidStatusError,
InvalidPropertyError,
- ExistingPropertyError
+ ExistingPropertyError,
+ NotFoundError
}
}
diff --git a/backend/core/src/Core.Domain/Repositories/ICustomerRepository.cs b/backend/core/src/Core.Domain/Repositories/ICustomerRepository.cs
index 8dc01c7..6322ae7 100644
--- a/backend/core/src/Core.Domain/Repositories/ICustomerRepository.cs
+++ b/backend/core/src/Core.Domain/Repositories/ICustomerRepository.cs
@@ -15,5 +15,7 @@ public interface ICustomerRepository
public Task> 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);
}
}
diff --git a/backend/core/src/Core.Infrastructure/AzureB2CGraphService/B2CGraphService.cs b/backend/core/src/Core.Infrastructure/AzureB2CGraphService/B2CGraphService.cs
new file mode 100644
index 0000000..3ce5372
--- /dev/null
+++ b/backend/core/src/Core.Infrastructure/AzureB2CGraphService/B2CGraphService.cs
@@ -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 _logger;
+
+ public B2CGraphService(
+ GraphServiceClient graphServiceClient,
+ IOptions options,
+ ILogger 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.");
+ }
+ }
+ }
+}
diff --git a/backend/core/src/Core.Infrastructure/AzureB2CGraphService/B2CServiceOptions.cs b/backend/core/src/Core.Infrastructure/AzureB2CGraphService/B2CServiceOptions.cs
new file mode 100644
index 0000000..266a61e
--- /dev/null
+++ b/backend/core/src/Core.Infrastructure/AzureB2CGraphService/B2CServiceOptions.cs
@@ -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; }
+ }
+}
diff --git a/backend/core/src/Core.Infrastructure/AzureB2CGraphService/IB2CGraphService.cs b/backend/core/src/Core.Infrastructure/AzureB2CGraphService/IB2CGraphService.cs
new file mode 100644
index 0000000..f0ed92f
--- /dev/null
+++ b/backend/core/src/Core.Infrastructure/AzureB2CGraphService/IB2CGraphService.cs
@@ -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);
+ }
+}
diff --git a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj
index 3ab2b2a..ce1e7eb 100644
--- a/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj
+++ b/backend/core/src/Core.Infrastructure/Core.Infrastructure.csproj
@@ -7,9 +7,11 @@
+
+
-
+
diff --git a/backend/core/src/Core.Infrastructure/Nexus/Enums.cs b/backend/core/src/Core.Infrastructure/Nexus/Enums.cs
index 0787ebf..7184a98 100644
--- a/backend/core/src/Core.Infrastructure/Nexus/Enums.cs
+++ b/backend/core/src/Core.Infrastructure/Nexus/Enums.cs
@@ -24,7 +24,8 @@ public enum NexusErrorCodes
TrustlevelNotFoundError = 4,
InvalidStatus = 4,
InvalidProperty = 5,
- TransactionNotFoundError = 6
+ TransactionNotFoundError = 6,
+ NonZeroAccountBalance = 7,
}
public enum MailStatus
diff --git a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusCustomerRepository.cs b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusCustomerRepository.cs
index 4d39e6f..4815fe3 100644
--- a/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusCustomerRepository.cs
+++ b/backend/core/src/Core.Infrastructure/Nexus/Repositories/NexusCustomerRepository.cs
@@ -5,9 +5,13 @@
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
{
@@ -15,11 +19,19 @@ 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)
@@ -144,6 +156,104 @@ public async Task> 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
+ {
+ { "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);
diff --git a/backend/core/src/Core.Presentation/Controllers/CustomersController.cs b/backend/core/src/Core.Presentation/Controllers/CustomersController.cs
index 1d551bc..ce6c93c 100644
--- a/backend/core/src/Core.Presentation/Controllers/CustomersController.cs
+++ b/backend/core/src/Core.Presentation/Controllers/CustomersController.cs
@@ -3,7 +3,6 @@
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
using Asp.Versioning;
-using Core.Application.Commands;
using Core.Application.Commands.CustomerCommands;
using Core.Application.Queries.CustomerQueries;
using Core.Presentation.Models;
@@ -88,6 +87,19 @@ public async Task GetCustomerTokenLimitsAsync()
return Ok(response);
}
+ [HttpDelete(Name = "DeleteCustomer")]
+ [ProducesResponseType(typeof(CustomResponse), 201)]
+ [ProducesResponseType(typeof(CustomErrorsResponse), 400)]
+ [ProducesResponseType(typeof(CustomErrorsResponse), 404)]
+ [ProducesResponseType(typeof(CustomErrorsResponse), 500)]
+ [RequiredScope("Customer.Delete")]
+ public async Task DeleteCustomerAsync()
+ {
+ var command = new DeleteCustomerCommand(GetUserId(), GetIP());
+ await _sender.Send(command);
+ return CreatedAtRoute("DeleteCustomer", null, new EmptyCustomResponse());
+ }
+
[HttpPost("devices", Name = "DeviceAuthentication")]
[ProducesResponseType(typeof(CustomResponse), 201)]
[ProducesResponseType(typeof(CustomErrorsResponse), 400)]
diff --git a/backend/core/tests/Core.InfrastructureTests/Helpers/NexusSDKHelper.cs b/backend/core/tests/Core.InfrastructureTests/Helpers/NexusSDKHelper.cs
index 91afb88..44f48da 100644
--- a/backend/core/tests/Core.InfrastructureTests/Helpers/NexusSDKHelper.cs
+++ b/backend/core/tests/Core.InfrastructureTests/Helpers/NexusSDKHelper.cs
@@ -49,6 +49,7 @@ public static AccountResponse AccountResponse(string customerCode)
public static SignableResponse SignableResponse()
{
var response = new BlockchainResponse(
+ "Code",
"TestHash",
"EncodedEnvelope",
new RequiredSignaturesResponse[]
diff --git a/backend/core/tests/Core.InfrastructureTests/Nexus/Repositories/NexusCustomerRepositoryTests.cs b/backend/core/tests/Core.InfrastructureTests/Nexus/Repositories/NexusCustomerRepositoryTests.cs
index 18d2062..c44fdeb 100644
--- a/backend/core/tests/Core.InfrastructureTests/Nexus/Repositories/NexusCustomerRepositoryTests.cs
+++ b/backend/core/tests/Core.InfrastructureTests/Nexus/Repositories/NexusCustomerRepositoryTests.cs
@@ -4,13 +4,18 @@
using Core.Domain.Entities.CustomerAggregate;
using Core.Domain.Exceptions;
+using Core.Infrastructure.AzureB2CGraphService;
+using Core.Infrastructure.Nexus;
using Core.Infrastructure.Nexus.Repositories;
+using Core.Infrastructure.Nexus.SigningService;
using Core.InfrastructureTests.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Nexus.Sdk.Shared.Requests;
using Nexus.Sdk.Shared.Responses;
using Nexus.Sdk.Token;
+using Nexus.Sdk.Token.Requests;
+using Nexus.Sdk.Token.Responses;
namespace Core.InfrastructureTests.Nexus.Repositories
{
@@ -26,7 +31,10 @@ public async Task CreateCustomer_Creates_Customer_TestAsync()
server.Setup(s => s.Customers.Create(It.IsAny(), It.IsAny()))
.Returns(Task.FromResult(NexusSDKHelper.PrivateCustomer("TestCustomer")));
- var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions);
+ var signingService = new Mock().Object;
+ var b2cGraphService = new Mock().Object;
+
+ var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions, signingService, b2cGraphService);
var customer = new Customer()
{
@@ -74,6 +82,9 @@ public async Task CreateCustomer_Creates_Business_TestAsync()
server.Setup(s => s.Customers.Create(It.IsAny(), It.IsAny()))
.Returns(Task.FromResult(NexusSDKHelper.PrivateCustomer("TestCustomer")));
+ var signingService = new Mock().Object;
+ var b2cGraphService = new Mock().Object;
+
var customer = new Customer()
{
CustomerCode = "TestCustomer",
@@ -90,7 +101,7 @@ public async Task CreateCustomer_Creates_Business_TestAsync()
}
};
- var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions);
+ var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions, signingService, b2cGraphService);
await repo.CreateAsync(customer);
var expected = new CreateCustomerRequest
@@ -119,7 +130,10 @@ public async Task CreateCustomer_Creating_Throws_ExistsPropertyError_TestAsync()
server.Setup(s => s.Customers.Exists("TestCustomer"))
.Returns(Task.FromResult(true));
- var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions);
+ var signingService = new Mock().Object;
+ var b2cGraphService = new Mock().Object;
+
+ var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions, signingService, b2cGraphService);
var customer = new Customer()
{
@@ -157,7 +171,10 @@ public async Task CreateCustomer_Creating_Throws_EmailExistsError_TestAsync()
filteringParameters: new Dictionary(),
records: [NexusSDKHelper.PrivateCustomer("TestCustomer123")])));
- var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions);
+ var signingService = new Mock().Object;
+ var b2cGraphService = new Mock().Object;
+
+ var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions, signingService, b2cGraphService);
var customer = new Customer()
{
@@ -196,7 +213,10 @@ public async Task CreateCustomer_Creating_EmailExists_StatusDeleted_Success_Test
filteringParameters: new Dictionary(),
records: [NexusSDKHelper.DeletedPrivateCustomer("TestCustomer")])));
- var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions);
+ var signingService = new Mock().Object;
+ var b2cGraphService = new Mock().Object;
+
+ var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions, signingService, b2cGraphService);
var customer = new Customer()
{
@@ -227,7 +247,10 @@ public async static Task Get_Returns_Valid_Customer_TestAsync()
server.Setup(s => s.Customers.Get("TestCustomer"))
.Returns(Task.FromResult(NexusSDKHelper.PrivateCustomer("TestCustomer")));
- var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions);
+ var signingService = new Mock().Object;
+ var b2cGraphService = new Mock().Object;
+
+ var repo = new NexusCustomerRepository(server.Object, DefaultOptions.TokenOptions, signingService, b2cGraphService);
var customer = await repo.GetAsync("TestCustomer");
diff --git a/mobile/src/context/AppStateContext.tsx b/mobile/src/context/AppStateContext.tsx
index c14ee74..f6153d9 100644
--- a/mobile/src/context/AppStateContext.tsx
+++ b/mobile/src/context/AppStateContext.tsx
@@ -1,3 +1,7 @@
+// 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
+
import {
createContext,
useContext,
diff --git a/mobile/src/navigation/__tests__/WelcomeStack.test.tsx b/mobile/src/navigation/__tests__/WelcomeStack.test.tsx
index 665bc8e..82ed611 100644
--- a/mobile/src/navigation/__tests__/WelcomeStack.test.tsx
+++ b/mobile/src/navigation/__tests__/WelcomeStack.test.tsx
@@ -1,3 +1,7 @@
+// 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
+
import { waitFor } from "@testing-library/react-native";
import WelcomeStackNavigator from "../WelcomeStack";
import * as functions from "../../utils/functions";
diff --git a/mobile/src/utils/functions.ts b/mobile/src/utils/functions.ts
index 9e0befc..d474a46 100644
--- a/mobile/src/utils/functions.ts
+++ b/mobile/src/utils/functions.ts
@@ -1,3 +1,7 @@
+// 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
+
import * as SecureStore from "expo-secure-store";
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512";