private ExchangeRateDto TryIndirectConversion(
            IReadOnlyCollection <ExchangeRateDto> exchangeRates,
            Currency fixedCurrency,
            Currency variableCurrency,
            DateTime dayOfConversion,
            ISystemProcessOperationRunRuleContext ruleCtx)
        {
            var fixedExchangeRates    = this.GetExchangeRates(exchangeRates, fixedCurrency);
            var variableExchangeRates = this.GetExchangeRates(exchangeRates, variableCurrency);

            var sharedVariableRateInitial = fixedExchangeRates.FirstOrDefault(
                ier => variableExchangeRates.Select(ter => ter.VariableCurrency).Contains(ier.VariableCurrency));

            if (sharedVariableRateInitial == null)
            {
                this._logger.LogError(
                    $"could not find a shared common currency using a one step approach for {fixedCurrency.Code} and {variableCurrency.Code} on {dayOfConversion}");

                ruleCtx.EventException(
                    $"could not find a shared common currency using a one step approach for {fixedCurrency.Code} and {variableCurrency.Code} on {dayOfConversion}");

                return(null);
            }

            var sharedVariableRateTarget = variableExchangeRates.FirstOrDefault(
                tec => string.Equals(
                    tec.VariableCurrency,
                    sharedVariableRateInitial.VariableCurrency,
                    StringComparison.InvariantCultureIgnoreCase));

            if (sharedVariableRateTarget == null)
            {
                this._logger.LogError(
                    $"could not find a shared common currency using a one step approach for {fixedCurrency.Code} and {variableCurrency.Code} on {dayOfConversion}");

                ruleCtx.EventException(
                    $"could not find a shared common currency using a one step approach for {fixedCurrency.Code} and {variableCurrency.Code} on {dayOfConversion}");

                return(null);
            }

            var reciprocalExchangeRate =

                // ReSharper disable once CompareOfFloatsByEqualityOperator
                sharedVariableRateTarget.Rate != 0 ? 1 / (decimal)sharedVariableRateTarget.Rate : 0;

            var completeRate = (double)reciprocalExchangeRate * sharedVariableRateInitial.Rate;

            return(new ExchangeRateDto
            {
                DateTime = sharedVariableRateTarget.DateTime,
                FixedCurrency = fixedCurrency.Code,
                VariableCurrency = variableCurrency.Code,
                Rate = completeRate
            });
        }
        private Money?TryIndirectConversion(
            IReadOnlyCollection <ExchangeRateDto> exchangeRates,
            Money initialMoney,
            Currency targetCurrency,
            DateTime dayOfConversion,
            ISystemProcessOperationRunRuleContext ruleCtx)
        {
            var initialExchangeRate = this.GetExchangeRates(exchangeRates, initialMoney.Currency);
            var targetExchangeRate  = this.GetExchangeRates(exchangeRates, targetCurrency);

            var sharedVariableRateInitial = initialExchangeRate.FirstOrDefault(
                ier => targetExchangeRate.Select(ter => ter.VariableCurrency).Contains(ier.VariableCurrency));

            if (sharedVariableRateInitial == null)
            {
                this._logger.LogError(
                    $"could not find a shared common currency using a one step approach for {initialMoney.Currency.Code} and {targetCurrency.Code} on {dayOfConversion}");

                ruleCtx.EventException(
                    $"could not find a shared common currency using a one step approach for {initialMoney.Currency.Code} and {targetCurrency.Code} on {dayOfConversion}");

                return(null);
            }

            var sharedVariableRateTarget = targetExchangeRate.FirstOrDefault(
                tec => string.Equals(
                    tec.VariableCurrency,
                    sharedVariableRateInitial.VariableCurrency,
                    StringComparison.InvariantCultureIgnoreCase));

            if (sharedVariableRateTarget == null)
            {
                this._logger.LogError(
                    $"could not find a shared common currency using a one step approach for {initialMoney.Currency.Code} and {targetCurrency.Code} on {dayOfConversion}");

                ruleCtx.EventException(
                    $"could not find a shared common currency using a one step approach for {initialMoney.Currency.Code} and {targetCurrency.Code} on {dayOfConversion}");

                return(null);
            }

            var variableCurrencyInitial = new Money(
                initialMoney.Value * (decimal)sharedVariableRateInitial.Rate,
                sharedVariableRateInitial.VariableCurrency);

            var reciprocalExchangeRate =

                // ReSharper disable once CompareOfFloatsByEqualityOperator
                sharedVariableRateTarget.Rate != 0 ? 1 / (decimal)sharedVariableRateTarget.Rate : 0;

            var fixedTargetCurrencyInitial = new Money(
                variableCurrencyInitial.Value * reciprocalExchangeRate,
                targetCurrency);

            return(fixedTargetCurrencyInitial);
        }
        private ExchangeRateDto Convert(
            IReadOnlyCollection <ExchangeRateDto> exchangeRates,
            Currency fixedCurrency,
            Currency variableCurrency,
            DateTime dayOfConversion,
            ISystemProcessOperationRunRuleContext ruleCtx)
        {
            // direct exchange rate i.e. we want to do USD to GBP and we have USD / GBP
            var directConversion = this.TryDirectConversion(exchangeRates, fixedCurrency, variableCurrency);

            if (directConversion != null)
            {
                this._logger.LogInformation(
                    $"was able to directly convert {fixedCurrency} to {variableCurrency} at rate of {directConversion.Rate} on {directConversion.DateTime}");

                return(directConversion);
            }

            // reciprocal exchange rate i.e. we want to do USD to GBP but we have GBP / USD
            var reciprocalConversion = this.TryReciprocalConversion(exchangeRates, fixedCurrency, variableCurrency);

            if (reciprocalConversion != null)
            {
                this._logger.LogInformation(
                    $"was able to reciprocally convert {fixedCurrency} to {variableCurrency} at rate of {reciprocalConversion.Rate} on {reciprocalConversion.DateTime}");

                return(reciprocalConversion);
            }

            // implicit exchange rate i.e. we want to do EUR to GBP but we have EUR / USD and GBP / USD
            var indirectConversion = this.TryIndirectConversion(
                exchangeRates,
                fixedCurrency,
                variableCurrency,
                dayOfConversion,
                ruleCtx);

            if (indirectConversion == null)
            {
                this._logger.LogError(
                    $"was unable to convert {fixedCurrency.Code} to {variableCurrency.Code} on {dayOfConversion}");
                ruleCtx.EventException(
                    $"was unable to convert {fixedCurrency.Code} to {variableCurrency.Code} on {dayOfConversion}");

                return(null);
            }

            this._logger.LogInformation(
                $"was able to indirectly convert {fixedCurrency} to {variableCurrency} at rate of {indirectConversion.Rate} on {indirectConversion.DateTime}");

            return(indirectConversion);
        }
        private async Task <IReadOnlyCollection <ExchangeRateDto> > GetExchangeRatesNearestToDate(
            DateTime dayOfRate,
            ISystemProcessOperationRunRuleContext ruleCtx)
        {
            dayOfRate = dayOfRate.Date;
            var exchRate = await this._exchangeRateApiRepository.GetAsync(dayOfRate, dayOfRate);

            // cycle through last two weeks of exchange rates
            var offset    = 0;
            var cycleDate = dayOfRate;

            while (!exchRate.ContainsKey(cycleDate) && offset < 15)
            {
                offset   += 1;
                cycleDate = cycleDate.AddDays(-1);
            }

            if (offset > 14)
            {
                this._logger.LogError($"could not find an exchange rate in the date range around {dayOfRate}.");
                ruleCtx.EventException($"could not find an exchange rate in the date range around {dayOfRate}.");

                return(new ExchangeRateDto[0]);
            }

            if (!exchRate.TryGetValue(cycleDate, out var rates))
            {
                this._logger.LogError(
                    $"could not find an exchange rate in the date range around {dayOfRate} in the dictionary.");
                ruleCtx.EventException(
                    $"could not find an exchange rate in the date range around {dayOfRate} in the dictionary.");

                return(new ExchangeRateDto[0]);
            }

            return(rates);
        }
        public async Task <Money?> Convert(
            IReadOnlyCollection <Money> monies,
            Currency targetCurrency,
            DateTime dayOfConversion,
            ISystemProcessOperationRunRuleContext ruleCtx)
        {
            if (monies == null || !monies.Any())
            {
                this._logger.LogInformation(
                    $"received null or empty currency amounts. Returning 0 currency amount in target currency of {targetCurrency} for rule {ruleCtx?.Id()}");
                return(new Money(0, targetCurrency));
            }

            if (string.IsNullOrWhiteSpace(targetCurrency.Code))
            {
                this._logger.LogError("asked to convert to a null or empty currency");
                return(monies.Aggregate((i, o) => new Money(i.Value + o.Value, i.Currency)));
            }

            if (monies.All(ca => Equals(ca.Currency, targetCurrency)))
            {
                this._logger.LogInformation(
                    "inferred all currency amounts matched the target currency. Aggregating trades and returning.");
                return(monies.Aggregate((i, o) => new Money(i.Value + o.Value, i.Currency)));
            }

            this._logger.LogInformation($"about to fetch exchange rates on {dayOfConversion}");
            var rates = await this.ExchangeRates(dayOfConversion, ruleCtx);

            if (rates == null || !rates.Any())
            {
                this._logger.LogError(
                    $"unable to change rates to {targetCurrency.Code} on {dayOfConversion.ToShortDateString()} due to missing rates");
                ruleCtx.EventException(
                    $"unable to change rates to {targetCurrency.Code} on {dayOfConversion.ToShortDateString()} due to missing rates");

                return(null);
            }

            var convertedToTargetCurrency = monies.Select(
                currency => this.Convert(rates, currency, targetCurrency, dayOfConversion, ruleCtx)).ToList();

            var totalInConvertedCurrency = convertedToTargetCurrency.Where(cc => cc.HasValue).Select(cc => cc.Value)
                                           .Sum(cc => cc.Value);

            this._logger.LogInformation($"returning {totalInConvertedCurrency} ({targetCurrency})");
            return(new Money(totalInConvertedCurrency, targetCurrency));
        }
        // fixed is basically from
        // variable is to
        // so EUR/USD 1.3225 means 1 euro buys 1.3225 dollars
        // with eur = fixed and usd = variable currencies
        public async Task <ExchangeRateDto> GetRate(
            Currency fixedCurrency,
            Currency variableCurrency,
            DateTime dayOfConversion,
            ISystemProcessOperationRunRuleContext ruleCtx)
        {
            if (string.IsNullOrWhiteSpace(fixedCurrency.Code) || string.IsNullOrWhiteSpace(variableCurrency.Code))
            {
                this._logger.LogError(
                    $"was asked to convert two currencies. Once of which was null or empty {fixedCurrency} {variableCurrency}");
                return(null);
            }

            if (string.Equals(fixedCurrency.Code, variableCurrency.Code, StringComparison.InvariantCultureIgnoreCase))
            {
                var noConversionRate = new ExchangeRateDto
                {
                    DateTime         = dayOfConversion,
                    FixedCurrency    = fixedCurrency.Code,
                    VariableCurrency = variableCurrency.Code,
                    Rate             = 1
                };

                this._logger.LogInformation(
                    $"was asked to convert two currencies but they were equal. Returning a rate of 1 for {fixedCurrency} and {variableCurrency}");

                return(noConversionRate);
            }

            var rates = await this.GetExchangeRatesNearestToDate(dayOfConversion, ruleCtx);

            if (rates == null || !rates.Any())
            {
                this._logger.LogError($"unable to find any rates on {dayOfConversion.ToShortDateString()}");
                ruleCtx.EventException(
                    $"unable to change rates from {fixedCurrency.Code} to {variableCurrency.Code} on {dayOfConversion.ToShortDateString()}");

                return(null);
            }

            var rate = this.Convert(rates, fixedCurrency, variableCurrency, dayOfConversion, ruleCtx);

            this._logger.LogInformation(
                $"was asked to convert two currencies {fixedCurrency} and {variableCurrency} on {dayOfConversion}. Returning {rate.Rate} as the exchange rate");

            return(rate);
        }
        private Money?Convert(
            IReadOnlyCollection <ExchangeRateDto> exchangeRates,
            Money initialMoney,
            Currency targetCurrency,
            DateTime dayOfConversion,
            ISystemProcessOperationRunRuleContext ruleCtx)
        {
            if (Equals(initialMoney.Currency, targetCurrency))
            {
                this._logger.LogInformation(
                    $"asked to convert {initialMoney.Currency} to {targetCurrency} and found they were the same. Returning initial amount.");
                return(initialMoney);
            }

            // direct exchange rate i.e. we want to do USD to GBP and we have USD / GBP
            var directConversion = this.TryDirectConversion(exchangeRates, initialMoney, targetCurrency);

            if (directConversion != null)
            {
                this._logger.LogInformation(
                    $"managed to directly convert {initialMoney.Currency} {initialMoney.Value} to {targetCurrency} {directConversion.Value.Value}.");
                return(directConversion);
            }

            this._logger.LogInformation(
                $"failed to directly convert {initialMoney.Currency} to {targetCurrency}. Trying reciprocal conversion.");

            // reciprocal exchange rate i.e. we want to do USD to GBP but we have GBP / USD
            var reciprocalConversion = this.TryReciprocalConversion(exchangeRates, initialMoney, targetCurrency);

            if (reciprocalConversion != null)
            {
                this._logger.LogInformation(
                    $"managed to reciprocally convert {initialMoney.Currency} {initialMoney.Value} to {targetCurrency} {reciprocalConversion.Value.Value}.");
                return(reciprocalConversion);
            }

            this._logger.LogInformation(
                $"failed to reciprocally convert {initialMoney.Currency} to {targetCurrency}. Trying indirect conversion.");

            // implicit exchange rate i.e. we want to do EUR to GBP but we have EUR / USD and GBP / USD
            var indirectConversion = this.TryIndirectConversion(
                exchangeRates,
                initialMoney,
                targetCurrency,
                dayOfConversion,
                ruleCtx);

            if (indirectConversion == null)
            {
                this._logger.LogError(
                    $"was unable to convert {initialMoney.Currency.Code} to {targetCurrency.Code} on {dayOfConversion} after attempting an indirect conversion. Returning null.");
                ruleCtx.EventException(
                    $"was unable to convert {initialMoney.Currency.Code} to {targetCurrency.Code} on {dayOfConversion}");

                return(null);
            }

            this._logger.LogInformation(
                $"managed to indirectly convert {initialMoney.Currency} {initialMoney.Value} to {targetCurrency} {indirectConversion.Value.Value}.");

            return(indirectConversion);
        }