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); }
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) }); }