Skip to content

Mykhailo96/code-examples

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 

Repository files navigation

Account Tokenization API service

API service provides a possibility to tokenize and securely a your account data. It contains multiple layers, here are some of them: 'Controllers' - describes endpoints (HTTP methods, URLs), input and output data, validation rules, possible response status codes and response body. 'Controller' layer does not contain any business logic, it is just an interface for the API clients. 'Services' - contains all business logic. It receives its input parameters from the 'Controller' layer. It is responsible for any calculations, sending events, communication with DAL etc.

API controller to tokenize and manage account data

[ApiVersion("1.0")]
[Authorize]
[Route("api/accounts")]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public sealed class AccountController : AppController
{
    private readonly IAccountService _accountService;

    public AccountController(IAccountService accountService)
    {
        _accountService = accountService;
    }

    /// <summary>
    /// Create a new account.
    /// Required scopes: token.
    /// </summary>
    /// <param name="accountCreateRequestVM"></param>
    /// <returns></returns>
    [HttpPost]
    [RequiredScope(Scopes.Token)]
    [ProducesResponseType(typeof(AccountTokenizeResponseVM), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(BadRequestVM), StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> CreateAccount([FromBody] AccountCreateRequestVM accountCreateRequestVM)
    {
        var tokenizeResult = await _accountService.CreateAccountAsync(
            accountCreateRequestVM.AccountNumber,
            accountCreateRequestVM.ExpirationDate,
            accountCreateRequestVM.HolderEmail,
            ClientId.Value);

        return tokenizeResult.Match<IActionResult>(
            token => Ok(new AccountTokenizeResponseVM { Token = token }),
            failedToTokenize => InternalServerError(failedToTokenize.Message()),
            invalidAccountNumber => BadRequest(nameof(AccountCreateRequestVM.AccountNumber), invalidAccountNumber.Message()));
    }

    /// <summary>
    /// Update account details.
    /// Required scopes: token.
    /// </summary>
    /// <param name="accountUpdateRequestVM"></param>
    /// <returns></returns>
    [HttpPut]
    [RequiredScope(Scopes.Token)]
    [ProducesResponseType(typeof(AccountUpdateResponseVM), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(BadRequestVM), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> UpdateAccount([FromBody] AccountUpdateRequestVM accountUpdateRequestVM)
    {
        var updateAccountResult = await _accountService.UpdateAccountAsync(
            accountUpdateRequestVM.AccountNumber,
            accountUpdateRequestVM.ExpirationDate,
            accountUpdateRequestVM.HolderEmail,
            accountUpdateRequestVM.Token,
            ClientId.Value);

        return updateAccountResult.Match<IActionResult>(
            token => Ok(new AccountUpdateResponseVM { Token = token }),
            accountNotFound => NotFound(accountNotFound.Message()),
            accountAlreadyExists => BadRequest(nameof(AccountUpdateRequestVM.AccountNumber), accountAlreadyExists.Message()),
            failedToUpdateAccount => InternalServerError(failedToUpdateAccount.Message()),
            invalidAccountNumber => BadRequest(nameof(AccountUpdateRequestVM.AccountNumber), invalidAccountNumber.Message()));
    }

    /// <summary>
    /// Receive a token by corresponding account number.
    /// Required scopes: token.
    /// </summary>
    /// <param name="tokenFindRequestVM"></param>
    /// <returns></returns>
    [HttpPost("find")]
    [RequiredScope(Scopes.Token)]
    [ProducesResponseType(typeof(TokenFindResponseVM), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(BadRequestVM), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> FindToken([FromBody] TokenFindRequestVM tokenFindRequestVM)
    {
        var findTokenResult = await _accountService.FindTokenAsync(tokenFindRequestVM.AccountNumber, ClientId.Value);

        return findTokenResult.Match<IActionResult>(
            tokenData => Ok(new TokenFindResponseVM { Token = tokenData }),
            tokenNotFound => NotFound(tokenNotFound.Message()),
            failedToFindToken => InternalServerError(failedToFindToken.Message()));
    }

    /// <summary>
    /// Delete account by corresponding token.
    /// Required scopes: token.
    /// </summary>
    /// <param name="accountDeleteRequestVM"></param>
    /// <returns></returns>
    [HttpDelete]
    [RequiredScope(Scopes.Token)]
    [ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(BadRequestVM), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> DisableAccount([FromBody] AccountDeleteRequestVM accountDeleteRequestVM)
    {
        var accountDisableResult = await _accountService.DisableAccountAsync(accountDeleteRequestVM.Token, ClientId.Value);

        return accountDisableResult.Match<IActionResult>(
            success => Ok(),
            accountNotFound => NotFound(accountNotFound.Message()));
    }
}

Account tokenization service with some business logic

internal sealed class TokenizationService : ITokenizationService
{
    ...
    
    public async Task<OneOf<
        CreateAccountResultDTO,
        FailedToTokenizeError,
        InvalidCardBrandError>>
    CreateAccountAsync(
        string accountNumber,
        string expirationDate,
        string holderEmail,
        short clientId,
        string token = null)
    {
        var visibleAccountNumberParts = GetVisibleAccountNumberParts(accountNumber);
    
        var findTokenResult = await FindTokenAsync(
            accountNumber,
            visibleAccountNumberParts.FirstEight,
            visibleAccountNumberParts.LastFour,
            clientId);
    
        if (!findTokenResult.Success) return new FailedToTokenizeError();
    
        var encryptedAccountDetails = await GetEncryptedAccountAsync(accountNumber, findTokenResult.Value);
        if (!encryptedAccountDetails.Success) return new FailedToTokenizeError();
    
        var accountExists = findTokenResult.Value is not null;
        if (accountExists)
        {
            await _mediator.Send(new AccountUpdateCommand(
                findTokenResult.Value.AccountId,
                visibleAccountNumberParts.FirstEight,
                visibleAccountNumberParts.LastFour,
                encryptedAccountDetails.Value.IV,
                encryptedAccountDetails.Value.AccountNumber,
                expirationDate,
                holderEmail));
    
            return new CreateAccountResultDTO()
            {
                AccountId = findTokenResult.Value.AccountId,
                Token = findTokenResult.Value.Token,
                NewAccountCreated = false
            };
        }
    
        var cardBinIdResult = await _cardBinService.GetCardBinIdAsync(visibleAccountNumberParts.FirstEight);
        if (!cardBinIdResult.Success) return new FailedToTokenizeError();
        if (!cardBinIdResult.Value.HasValue) return new InvalidCardBrandError();
    
        var newtoken = GenerateToken();
    
        var accountId = await _mediator.Send(new AccountCreateCommand(
            visibleAccountNumberParts.FirstEight,
            visibleAccountNumberParts.LastFour,
            newtoken,
            encryptedAccountDetails.Value.AccountNumber,
            encryptedAccountDetails.Value.IV,
            encryptedAccountDetails.Value.DataEncryptionKeyId,
            expirationDate,
            holderEmail,
            clientId,
            cardBinIdResult.Value.Value,
            token));
    
        return new CreateAccountResultDTO()
        {
            AccountId = accountId,
            Token = newtoken,
            NewAccountCreated = true
        };
    
    ...
}

Integration Event handler

Our system includes multiple services that uses integration events to communicate changes between them. To handle multiple integration events of different types we should provide a 'Chain of Responsibility' flow for each listener. To implement such listeners I've created a new background app (Hosted Service) that is subscribed to multiple topics to receive and handle async events from other services/apps. It reads events from SQS queues and uses a custom implementation of 'Chain of Responsibility' that can be easily integrated with .NET DI.

Extend DI with own Chain of Responsibility. You can registed as many handlers as you want. Just make sure that handlers order is correct.

services.AddChain()
    .AddHandler<AccountCreatedHandler>()
    .AddHandler<AccountUpdatedHandler>()
    .AddHandler<AccountDeletedHandler>();

Custom Handler Registry and Manager to register custom event handlers and their dependencies.

public static class Extensions
{
    public static ChainConfiguration AddChain(this IServiceCollection services)
    {
        HandlerRegistry registry = new();
    
        services.AddSingleton<IHandlerManager>(serviceProvider =>
            new HandlerManager(serviceProvider, registry));
    
        return new ChainConfiguration(services, registry);
    }
}

internal sealed class HandlerManager : IHandlerManager
{
    private readonly IServiceProvider _serviceProvider;
    private readonly HandlerRegistry _handlerRegistry;

    public HandlerManager(IServiceProvider serviceProvider, HandlerRegistry handlerRegistry)
    {
        _serviceProvider = serviceProvider;
        _handlerRegistry = handlerRegistry;
    }

    public async Task<bool> HandleAsync(JsonDocument eventToHandle)
    {
        foreach (var handlerType in _handlerRegistry)
        {
            if (_serviceProvider.GetService(handlerType) is not IHandler handler) continue;

            if (handler.CanHandle(eventToHandle))
            {
                return await handler.HandleAsync(eventToHandle);
            }
        }

        return false;
    }
}

Event Handler. Receive an event, check its type and process if required.

internal sealed class AccountCreatedHandler : IHandler
{
    private readonly ILogService _logService;
    private readonly IRegistryService _registryService;

    public AccountCreatedHandler(ILogService logService, IRegistryService registryService)
    {
        _logService = logService;
        _registryService = registryService;
    }

    public bool CanHandle(JsonDocument eventToHandle)
    {
        var eventTypeExists = eventToHandle.RootElement.TryGetProperty(IntegrationEventFields.EventType, out var eventType);

        return eventTypeExists && eventType.ToString() == IntegrationEventTypes.AccountCreated;
    }

    public async Task<bool> HandleAsync(JsonDocument eventToHandle)
    {
        var integrationEvent = JsonSerializer.Deserialize<AccountCreatedIntegrationEvent>(eventToHandle);

        var registerResult = await _registryService.RegisterAsync(
            integrationEvent.ClientId,
            integrationEvent.AccountId);

        if (registerResult.Value is FailedToTokenizeError)
        {
            _logService.LogError("Cannot process integration event", obj: integrationEvent);
        }

        return registerResult.Match(
            _ => true,
            failedToCreate => false);
    }
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published