private async Task <CustomerBalanceResultModel> InternalGetAsync(string customerId)
        {
            var value = await GetCachedValue(customerId);

            if (value != null)
            {
                return(value.DeserializeJson <CustomerBalanceResultModel>());
            }

            var walletAddressResult = await _walletsService.GetCustomerWalletAsync(customerId);

            switch (walletAddressResult.Error)
            {
            case CustomerWalletAddressError.CustomerWalletMissing:
                return(CustomerBalanceResultModel.Failed(CustomerBalanceError.CustomerWalletMissing));

            case CustomerWalletAddressError.InvalidCustomerId:
                return(CustomerBalanceResultModel.Failed(CustomerBalanceError.InvalidCustomerId));

            case CustomerWalletAddressError.None:
                break;

            default:
                throw new ArgumentOutOfRangeException(nameof(walletAddressResult.Error));
            }

            var balanceResponse =
                await _quorumOperationExecutorClient.AddressesApi.GetBalanceForAddressAsync(walletAddressResult.WalletAddress);

            var transfersInProgress = await _operationsFetcher.GetTransfersInProgressAsync(walletAddressResult.WalletAddress);

            var transfersInProgressAmount = transfersInProgress
                                            .Select(x => JsonConvert.DeserializeObject <TokensTransferContext>(x.ContextJson))
                                            .Sum(x => x.Amount);

            var seizedAmount = (await _operationsFetcher.GetSeizeOperationsInProgressAsync(customerId))
                               .Select(x => JsonConvert.DeserializeObject <SeizeToInternalContext>(x.ContextJson))
                               .Sum(x => x.Amount);

            var reservedAmount = transfersInProgressAmount + seizedAmount;

            if (balanceResponse.Balance < reservedAmount)
            {
                _log.Warning(
                    $"The reserved amount ({reservedAmount}) is more than actual balance ({balanceResponse.Balance})",
                    context: new { customerId });
            }

            var availableBalance = balanceResponse.Balance - balanceResponse.StakedBalance >= reservedAmount
                ? balanceResponse.Balance - balanceResponse.StakedBalance - reservedAmount
                : 0;

            var result = CustomerBalanceResultModel.Succeeded(availableBalance, balanceResponse.StakedBalance);

            await SetCacheValueAsync(customerId, result);

            return(result);
        }
        public async Task <CustomerBalanceResultModel> GetAsync(string customerId)
        {
            if (string.IsNullOrEmpty(customerId))
            {
                return(CustomerBalanceResultModel.Failed(CustomerBalanceError.InvalidCustomerId));
            }

            return(await InternalGetAsync(customerId));
        }
        public async Task HandleAsync_ErrorWhenGettingTheBalance_WalletOwnersRepoNotCalled()
        {
            _balanceServiceMock.Setup(x => x.GetAsync(FakeCustomerId))
            .ReturnsAsync(CustomerBalanceResultModel.Failed(CustomerBalanceError.CustomerWalletMissing));

            var sut = CreateSutInstance();

            await sut.HandleAsync(FakeCustomerId);

            _walletOwnersRepoMock.Verify(x => x.GetByOwnerIdAsync(FakeCustomerId), Times.Never);
        }
        public async Task HandleAsync_CustomerHas0Balance_PublisherCalledAndWalletOwnersRepoNotCalled()
        {
            _balanceServiceMock.Setup(x => x.GetAsync(FakeCustomerId))
            .ReturnsAsync(CustomerBalanceResultModel.Succeeded(0, 0));

            var sut = CreateSutInstance();

            await sut.HandleAsync(FakeCustomerId);

            _seizeBalanceFromCustomerCompletedPublisher.Verify(x =>
                                                               x.PublishAsync(It.Is <SeizeBalanceFromCustomerCompletedEvent>(e => e.CustomerId == FakeCustomerId)));
            _walletOwnersRepoMock.Verify(x => x.GetByOwnerIdAsync(FakeCustomerId), Times.Never);
        }
        public async Task HandleAsync_WalletOwnerDoesNotExist_OperationProducerNotCalled()
        {
            _balanceServiceMock.Setup(x => x.GetAsync(FakeCustomerId))
            .ReturnsAsync(CustomerBalanceResultModel.Succeeded(FakeBalance, 0));

            _walletOwnersRepoMock.Setup(x => x.GetByOwnerIdAsync(FakeCustomerId))
            .ReturnsAsync((IWalletOwner)null);

            var sut = CreateSutInstance();

            await sut.HandleAsync(FakeCustomerId);

            _operationRequestProducerMock.Verify(
                x => x.AddAsync(FakeCustomerId, OperationType.SeizeToInternal, It.IsAny <SeizeToInternalContext>(),
                                null), Times.Never);
        }
        public async Task TransferAsync_BalanceServiceReturnsError_ReturnsFail(CustomerBalanceError balanceError, TransferError expectedError)
        {
            _walletsServiceMock
            .Setup(x => x.GetCustomerWalletAsync(It.IsAny <string>()))
            .ReturnsAsync(CustomerWalletAddressResultModel.Succeeded(FakeWalletAddress));

            _balanceServiceMock
            .Setup(x => x.GetAsync(It.IsAny <string>()))
            .ReturnsAsync(CustomerBalanceResultModel.Failed(balanceError));

            var sut = CreateSutInstance();

            var result = await sut.P2PTransferAsync(FakeSenderCustomerId, FakeRecipientCustomerId, 1, FakeTransferId);

            Assert.Equal(expectedError, result.Error);
        }
        public async Task TransferToExternalAsync_SenderWalletNotEnoughFunds_ReturnsFail(long transferAmount, long fee, long balanceAmount)
        {
            _walletsServiceMock
            .Setup(x => x.GetCustomerWalletAsync(It.IsAny <string>()))
            .ReturnsAsync(CustomerWalletAddressResultModel.Succeeded(FakeWalletAddress));

            _balanceServiceMock
            .Setup(x => x.GetAsync(It.IsAny <string>()))
            .ReturnsAsync(CustomerBalanceResultModel.Succeeded(balanceAmount, FakeStakedAmount));

            var sut = CreateSutInstance();

            var result = await sut.TransferToExternalAsync(FakeSenderCustomerId, FakeWalletAddress, transferAmount, fee, FakeTransferId);

            Assert.Equal(TransferError.NotEnoughFunds, result.Error);
        }
        public async Task HandleAsync__OperationProducerCalled()
        {
            _balanceServiceMock.Setup(x => x.GetAsync(FakeCustomerId))
            .ReturnsAsync(CustomerBalanceResultModel.Succeeded(FakeBalance, 0));

            _walletOwnersRepoMock.Setup(x => x.GetByOwnerIdAsync(FakeCustomerId))
            .ReturnsAsync(new WalletOwnerEntity
            {
                WalletId = FakeWalletId
            });

            var sut = CreateSutInstance();

            await sut.HandleAsync(FakeCustomerId);

            _operationRequestProducerMock.Verify(
                x => x.AddAsync(FakeCustomerId, OperationType.SeizeToInternal,
                                It.Is <SeizeToInternalContext>(c => c.Account == FakeWalletId && c.Amount == FakeBalance),
                                null), Times.Once);
        }
示例#9
0
        public async Task HandleAsync_OperationFound_FinishesSuccessfully()
        {
            _walletOwnersRepositoryMock
            .Setup(x => x.GetByWalletAddressAsync(It.IsAny <string>()))
            .ReturnsAsync(new WalletOwnerEntity());

            _balanceServiceMock
            .Setup(x => x.ForceBalanceUpdateAsync(It.IsAny <string>(), It.IsAny <OperationType>(), It.IsAny <Guid>()))
            .ReturnsAsync(CustomerBalanceResultModel.Succeeded(ValidBalance, FakeStakedAmount))
            .Verifiable();

            _operationsFetcherMock
            .Setup(x => x.GetByHashAsync(It.IsAny <string>()))
            .ReturnsAsync(new OperationEntity
            {
                ContextJson = new TokensTransferContext {
                    RequestId = "whatever"
                }.ToJson()
            });

            _transferDetectedPublisherMock
            .Setup(x => x.PublishAsync(It.IsAny <TransferDetectedEvent>()))
            .Returns(Task.CompletedTask)
            .Verifiable();

            var sut = CreateSutInstance();

            await sut.HandleAsync(
                FakeWalletAddress1,
                FakeWalletAddress2,
                ValidAmount,
                ValidTransactionHash,
                DateTime.UtcNow);

            _balanceServiceMock.Verify(
                x => x.ForceBalanceUpdateAsync(It.IsAny <string>(), It.IsAny <OperationType>(), It.IsAny <Guid>()),
                Times.Exactly(2));
            _p2PTransferPublisherMock.Verify(x => x.PublishAsync(It.IsAny <P2PTransferDetectedEvent>()), Times.Once);
            _transferDetectedPublisherMock.Verify(x => x.PublishAsync(It.IsAny <TransferDetectedEvent>()), Times.Once);
        }
        public async Task TransferToExternalAsync_IsDuplicateCheck_WorksCorrectly(bool isDuplicate, TransferError expectedError)
        {
            const long amount = 100;

            _walletsServiceMock
            .Setup(x => x.GetCustomerWalletAsync(It.IsAny <string>()))
            .ReturnsAsync(CustomerWalletAddressResultModel.Succeeded(FakeWalletAddress));

            _balanceServiceMock
            .Setup(x => x.GetAsync(It.IsAny <string>()))
            .ReturnsAsync(CustomerBalanceResultModel.Succeeded(amount, FakeStakedAmount));

            _deduplicationLogMock
            .Setup(x => x.IsDuplicateAsync(It.IsAny <string>()))
            .ReturnsAsync(isDuplicate);

            var sut = CreateSutInstance();

            var result = await sut.TransferToExternalAsync(FakeSenderCustomerId, FakeWalletAddress, amount, 0, FakeTransferId);

            Assert.Equal(expectedError, result.Error);
        }
        public async Task <CustomerBalanceResultModel> ForceBalanceUpdateAsync(string customerId, OperationType operationCausedUpdate, Guid operationId)
        {
            if (string.IsNullOrEmpty(customerId))
            {
                return(CustomerBalanceResultModel.Failed(CustomerBalanceError.InvalidCustomerId));
            }

            if (!AllowedOperationTypesToUpdateBalanceMap.ContainsKey(operationCausedUpdate))
            {
                throw new InvalidOperationException(
                          $"Attempt to update balance with wrong operation type, operationId={operationId}, operationType={operationCausedUpdate.ToString()}");
            }

            await _distributedCache.RemoveAsync(BuildCacheKey(customerId));

            var balanceResult = await InternalGetAsync(customerId);

            if (balanceResult.Error != CustomerBalanceError.None)
            {
                _log.Error(message: "Couldn't update balance",
                           context: new { customerId, error = balanceResult.Error.ToString() });
            }
            else
            {
                await _customerBalanceUpdatedPublisher.PublishAsync(new CustomerBalanceUpdatedEvent
                {
                    CustomerId  = customerId,
                    Balance     = balanceResult.Total,
                    Timestamp   = DateTime.UtcNow,
                    Reason      = AllowedOperationTypesToUpdateBalanceMap[operationCausedUpdate],
                    OperationId = operationId
                });
            }

            return(balanceResult);
        }
 private async Task SetCacheValueAsync(string customerId, CustomerBalanceResultModel value)
 {
     await _distributedCache.SetStringAsync(BuildCacheKey(customerId),
                                            value.ToJson(),
                                            new DistributedCacheEntryOptions { AbsoluteExpiration = DateTime.UtcNow.Add(_cacheExpirationPeriod) });
 }