internal static List <LimitationType> MapOperationType(CurrencyOperationType currencyOperationType) { var result = new List <LimitationType>(); switch (currencyOperationType) { case CurrencyOperationType.CardCashIn: result.Add(LimitationType.CardAndSwiftCashIn); result.Add(LimitationType.CardCashIn); break; case CurrencyOperationType.SwiftTransfer: result.Add(LimitationType.CardAndSwiftCashIn); break; case CurrencyOperationType.CryptoCashOut: result.Add(LimitationType.CryptoCashOut); break; case CurrencyOperationType.CardCashOut: case CurrencyOperationType.CryptoCashIn: break; case CurrencyOperationType.SwiftTransferOut: break; default: throw new NotSupportedException($"Currency operation type {currencyOperationType} is not supported!"); } return(result); }
public async Task <(double, bool)> GetCurrentAmountAsync( string clientId, string asset, LimitationPeriod period, CurrencyOperationType operationType) { (var items, bool notCached) = await _data.GetClientDataAsync(clientId, operationType); double result = 0; DateTime now = DateTime.UtcNow; foreach (var item in items) { if (item.Asset != asset) { continue; } if (period == LimitationPeriod.Day && now.Subtract(item.DateTime).TotalHours >= 24) { continue; } result += item.Volume; } return(result, notCached); }
public async Task RemoveOperationAsync( string clientId, string asset, double amount, CurrencyOperationType operationType) { var keys = await GetClientAttemptKeysAsync(clientId); if (keys.Length == 0) { return; } var attemptJsons = await _db.StringGetAsync(keys); var attemptToDelete = attemptJsons .Where(a => a.HasValue) .Select(a => a.ToString().DeserializeJson <CurrencyOperationAttempt>()) .Where(i => i.OperationType == operationType && i.Asset == asset && Math.Abs(amount - i.Amount) < _minDiff) .OrderBy(i => i.DateTime) .FirstOrDefault(); if (attemptToDelete == null) { return; } string clientKey = string.Format(_clientSetKeyPattern, _instanceName, clientId); string timeStr = attemptToDelete.DateTime.ToString(_dataFormat); string attemptSuffix = string.Format(_attemptKeySuffixPattern, operationType, timeStr); var attemptKey = $"{clientKey}:{attemptSuffix}"; var tx = _db.CreateTransaction(); var tasks = new List <Task> { tx.SortedSetRemoveAsync(clientKey, attemptSuffix), tx.KeyDeleteAsync(attemptKey), }; if (await tx.ExecuteAsync()) { await Task.WhenAll(tasks); } }
public async Task AddDataAsync( string clientId, string asset, double amount, int ttlInMinutes, CurrencyOperationType operationType) { amount = Math.Abs(amount); var now = DateTime.UtcNow; var attempt = new CurrencyOperationAttempt { ClientId = clientId, Asset = asset, Amount = amount, OperationType = operationType, DateTime = now, }; string clientKey = string.Format(_clientSetKeyPattern, _instanceName, clientId); string attemptSuffix = string.Format(_attemptKeySuffixPattern, operationType, now.ToString(_dataFormat)); var attemptKey = $"{clientKey}:{attemptSuffix}"; var tx = _db.CreateTransaction(); var tasks = new List <Task> { tx.SortedSetAddAsync(clientKey, attemptSuffix, DateTime.UtcNow.Ticks) }; var setKeyTask = tx.StringSetAsync(attemptKey, attempt.ToJson(), TimeSpan.FromMinutes(ttlInMinutes)); tasks.Add(setKeyTask); if (!await tx.ExecuteAsync()) { throw new InvalidOperationException($"Error during attempt adding for client {clientId}"); } await Task.WhenAll(tasks); if (!setKeyTask.Result) { throw new InvalidOperationException($"Error during attempt adding for client {clientId}"); } }
public Task CacheClientDataAsync(string clientId, CurrencyOperationType operationType) { return(_data.CacheClientDataIfRequiredAsync(clientId, operationType)); }
public async Task <(double, bool)> GetCurrentAmountAsync( string clientId, string asset, LimitationPeriod period, CurrencyOperationType operationType, bool checkAllCrypto = false) { int sign; switch (operationType) { case CurrencyOperationType.CardCashIn: case CurrencyOperationType.CryptoCashIn: case CurrencyOperationType.SwiftTransfer: sign = 1; break; case CurrencyOperationType.CardCashOut: case CurrencyOperationType.CryptoCashOut: case CurrencyOperationType.SwiftTransferOut: sign = -1; break; default: throw new NotSupportedException($"Operation type {operationType} can't be mapped to CashFlowDirection!"); } (var items, bool notCached) = await _data.GetClientDataAsync(clientId, operationType); DateTime now = DateTime.UtcNow; double result = 0; foreach (var item in items) { if (period == LimitationPeriod.Day && now.Subtract(item.DateTime).TotalHours >= 24) { continue; } if (Math.Sign(item.Volume) != Math.Sign(sign)) { continue; } double amount; if (checkAllCrypto) { if (!_currencyConverter.IsNotConvertible(item.Asset)) { continue; } var(_, convertedAmount) = await _currencyConverter.ConvertAsync( item.Asset, _currencyConverter.DefaultAsset, item.Volume, true); amount = convertedAmount; } else { if (item.Asset != asset) { continue; } amount = item.Volume; } result += amount; } return(Math.Abs(result), notCached); }
public async Task <LimitationCheckResult> CheckCashOperationLimitAsync( string clientId, string assetId, double amount, CurrencyOperationType currencyOperationType) { var originalAsset = assetId; var originalAmount = amount; amount = Math.Abs(amount); if (currencyOperationType == CurrencyOperationType.CryptoCashOut) { var asset = _assets.Get(assetId); if (amount < asset.CashoutMinimalAmount) { var minimalAmount = asset.CashoutMinimalAmount.GetFixedAsString(asset.Accuracy).TrimEnd('0'); return(new LimitationCheckResult { IsValid = false, FailMessage = $"The minimum amount to cash out is {minimalAmount}" }); } if (asset.LowVolumeAmount.HasValue && amount < asset.LowVolumeAmount) { var settings = await _limitSettingsRepository.GetAsync(); var timeout = TimeSpan.FromMinutes(settings.LowCashOutTimeoutMins); var callHistory = await _callTimeLimitsRepository.GetCallHistoryAsync("CashOutOperation", clientId, timeout); var cashoutEnabled = !callHistory.Any() || callHistory.IsCallEnabled(timeout, settings.LowCashOutLimit); if (!cashoutEnabled) { return new LimitationCheckResult { IsValid = false, FailMessage = "You have exceeded cash out operations limit. Please try again later." } } ; } } if (currencyOperationType == CurrencyOperationType.SwiftTransferOut) { var error = await CheckSwiftWithdrawLimitations(assetId, (decimal)amount); if (error != null) { return(new LimitationCheckResult { IsValid = false, FailMessage = error }); } } if (currencyOperationType != CurrencyOperationType.CryptoCashIn && currencyOperationType != CurrencyOperationType.CryptoCashOut) { (assetId, amount) = await _currencyConverter.ConvertAsync( assetId, _currencyConverter.DefaultAsset, amount); } var limitationTypes = LimitMapHelper.MapOperationType(currencyOperationType); var typeLimits = _limits.Where(l => limitationTypes.Contains(l.LimitationType)).ToList(); if (!typeLimits.Any()) { try { await _limitOperationsApi.AddOperationAttemptAsync( clientId, originalAsset, originalAmount, currencyOperationType == CurrencyOperationType.CardCashIn ?CashOperationsTimeoutInMinutes : _attemptRetainInMinutes, currencyOperationType); } catch (Exception ex) { _log.Error(ex, context: new { Type = "Attempt", clientId, originalAmount, originalAsset }); } return(new LimitationCheckResult { IsValid = true }); } //To handle parallel request await _lock.WaitAsync(); try { var assetLimits = typeLimits.Where(l => l.Asset == assetId).ToList(); string error = await DoPeriodCheckAsync( assetLimits, LimitationPeriod.Month, clientId, assetId, amount, currencyOperationType, false); if (error != null) { return new LimitationCheckResult { IsValid = false, FailMessage = error } } ; error = await DoPeriodCheckAsync( assetLimits, LimitationPeriod.Day, clientId, assetId, amount, currencyOperationType, false); if (error != null) { return new LimitationCheckResult { IsValid = false, FailMessage = error } } ; if (currencyOperationType == CurrencyOperationType.CryptoCashOut) { assetLimits = typeLimits.Where(l => l.Asset == _currencyConverter.DefaultAsset).ToList(); var(assetTo, convertedAmount) = await _currencyConverter.ConvertAsync( assetId, _currencyConverter.DefaultAsset, amount, true); error = await DoPeriodCheckAsync( assetLimits, LimitationPeriod.Month, clientId, assetTo, convertedAmount, currencyOperationType, true); if (error != null) { return new LimitationCheckResult { IsValid = false, FailMessage = error } } ; error = await DoPeriodCheckAsync( assetLimits, LimitationPeriod.Day, clientId, assetTo, convertedAmount, currencyOperationType, true); if (error != null) { return new LimitationCheckResult { IsValid = false, FailMessage = error } } ; } try { await _limitOperationsApi.AddOperationAttemptAsync( clientId, originalAsset, originalAmount, currencyOperationType == CurrencyOperationType.CardCashIn ?CashOperationsTimeoutInMinutes : _attemptRetainInMinutes, currencyOperationType); } catch (Exception ex) { _log.Error(ex, context: new { Type = "Attempt", clientId, originalAmount, originalAsset }); } } finally { _lock.Release(); } return(new LimitationCheckResult { IsValid = true }); }
private async Task <string> DoPeriodCheckAsync( IEnumerable <CashOperationLimitation> limits, LimitationPeriod period, string clientId, string asset, double amount, CurrencyOperationType currencyOperationType, bool checkAllCrypto) { var periodLimits = limits.Where(l => l.Period == period).ToList(); if (!periodLimits.Any()) { return(null); } (double cashPeriodValue, bool cashOperationsNotCached) = await _cashOperationsCollector.GetCurrentAmountAsync( clientId, asset, period, currencyOperationType, checkAllCrypto); (double transferPeriodValue, bool cashTransfersNotCached) = await _cashTransfersCollector.GetCurrentAmountAsync( clientId, asset, period, currencyOperationType); if (cashOperationsNotCached || cashTransfersNotCached) { try { await _limitOperationsApi.CacheClientDataAsync(clientId, currencyOperationType); } catch (Exception ex) { _log.Error(ex, context: new { Type = "CachOp", clientId, currencyOperationType }); } } var clientLimit = periodLimits.FirstOrDefault(l => l.ClientId == clientId); if (clientLimit != null) { string checkMessage = await CheckLimitAsync( cashPeriodValue, transferPeriodValue, clientLimit, period, clientId, asset, amount); if (!string.IsNullOrWhiteSpace(checkMessage)) { return(checkMessage); } } else { foreach (var periodLimit in periodLimits) { if (periodLimit.ClientId != null) { continue; } string checkMessage = await CheckLimitAsync( cashPeriodValue, transferPeriodValue, periodLimit, period, clientId, asset, amount); if (!string.IsNullOrWhiteSpace(checkMessage)) { return(checkMessage); } } } return(null); }