public IEnumerable <EfficiencyRecord> Compute([NotNull] IEnumerable <EfficiencyShift> shifts, LaborRate[] laborRates, DateTime start, DateTime end)
        {
            // TODO: This can be configurable
            var breakdownInterval = TimeSpan.FromHours(1);

            var results = new List <EfficiencyRecord>();

            foreach (var shift in shifts)
            {
                Transaction shiftFirstTransaction  = null;
                Transaction shiftLastTransaction   = null;
                var         shiftEfficiencyResults = new List <EfficiencyRecord>();

                var shiftIntervals = DateRangeList.GenerateAscending(shift.StartTime, shift.EndTime, breakdownInterval, nearest: true);

                foreach (var shiftInterval in shiftIntervals)
                {
                    var intervalTransactions = _transactionProvider.FindTransactions(shift.SiteCode,
                                                                                     shift.SiteEmployeeCodes, shiftInterval.Start, shiftInterval.End.AddMilliseconds(-1) //use EndOf.AddMilliseconds(-1) here so that we don't count same transaction for different intervals
                                                                                     ).ToArray();

                    if (intervalTransactions.Any())
                    {
                        if (shiftFirstTransaction == null)
                        {
                            shiftFirstTransaction = intervalTransactions.First();

                            // For the first transaction in a shift the transition time is not relevant, since
                            // we count it in the time to first transaction.
                            shiftFirstTransaction.TransitionTimeSeconds = 0;
                        }

                        shiftLastTransaction = intervalTransactions.Last();
                    }

                    var intervalEfficiencyResults = HandleInterval(shift, shiftInterval, intervalTransactions, laborRates);

                    shiftEfficiencyResults.AddRange(intervalEfficiencyResults);
                }

                shiftEfficiencyResults.ForEach(x => x.LastTransactionDate = shiftLastTransaction?.TransactionDate);

                //set time clocked-in for shifts with no transactions
                if (shiftEfficiencyResults.All(x => x.QuantityProcessed == 0))
                {
                    foreach (var effitem in shiftEfficiencyResults)
                    {
                        effitem.TimeClockedIn = TimeSpan.FromSeconds(Convert.ToInt32((effitem.IntervalEndTime - effitem.IntervalStartTime).TotalSeconds));
                    }
                }

                //set time to first for shifts with no transactions that count towards efficiency
                if (shiftEfficiencyResults.All(x => x.QuantityProcessed == 0 && x.TimeEarned.TotalSeconds == 0))
                {
                    foreach (var effitem in shiftEfficiencyResults)
                    {
                        effitem.ShiftTimeToFirstTransaction = effitem.TimeClockedIn;
                        effitem.IsNonProductiveClockIn      = shift.IsTransactional;
                    }
                }

                if (shiftEfficiencyResults.Any(x => x.QuantityProcessed > 0))
                {
                    // Set the time to the first and last transactions for this shift
                    var firstTransactionEfficiencyRecords = shiftEfficiencyResults.Where(e =>
                                                                                         (e.TransactionTypeCode == null || e.TransactionTypeCode.IgnoreCaseEquals(shiftFirstTransaction?.TransactionTypeCode)) &&
                                                                                         e.IntervalStartTime <= shiftFirstTransaction?.TransactionDate);

                    if (firstTransactionEfficiencyRecords != null && shiftFirstTransaction != null && firstTransactionEfficiencyRecords.Any())
                    {
                        foreach (var item in firstTransactionEfficiencyRecords)
                        {
                            var timeClockedIn = TimeSpan.FromSeconds(Convert.ToInt32((item.IntervalEndTime - item.IntervalStartTime).TotalSeconds));
                            var shiftTimeToFirstTransaction = item.QuantityProcessed == 0 ? timeClockedIn : item.IntervalStartToFirstTransaction;
                            item.ShiftTimeToFirstTransaction = shift.IsTransactional ? shiftTimeToFirstTransaction : new TimeSpan();
                            item.TimeClockedIn      += shiftTimeToFirstTransaction;
                            item.TransactionTypeCode = item.TransactionTypeCode ?? shiftFirstTransaction.TransactionTypeCode;
                        }
                    }

                    var lastTransactionEfficiencyRecords = shiftEfficiencyResults.Where(e =>
                                                                                        (e.TransactionTypeCode == null || e.TransactionTypeCode.IgnoreCaseEquals(shiftLastTransaction?.TransactionTypeCode)) &&
                                                                                        e.IntervalEndTime >= shiftLastTransaction?.TransactionDate);

                    if (lastTransactionEfficiencyRecords != null && shiftLastTransaction != null && lastTransactionEfficiencyRecords.Any())
                    {
                        foreach (var item in lastTransactionEfficiencyRecords)
                        {
                            var timeClockedIn = TimeSpan.FromSeconds(Convert.ToInt32((item.IntervalEndTime - item.IntervalStartTime).TotalSeconds));
                            var shiftTimeAfterLastTransaction = item.QuantityProcessed == 0 ? timeClockedIn : item.LastTransactionToIntervalEnd;
                            item.ShiftTimeAfterLastTransaction = shift.IsTransactional && !shift.IsClockedIn ? shiftTimeAfterLastTransaction : new TimeSpan();
                            item.TimeClockedIn      += shiftTimeAfterLastTransaction;
                            item.TransactionTypeCode = item.TransactionTypeCode ?? shiftLastTransaction.TransactionTypeCode;
                        }
                    }
                }

                results.AddRange(shiftEfficiencyResults
                                 .Where(x => x.TransactionTypeCode != null || x.TimeClockedIn.TotalSeconds != 0)); //filter out rows that don't have any seconds clocked in and ttype is null
            }

            SetStartEndShiftTime(results, start, end);

            return(results);
        }
 private static bool IsReworkTransaction(Transaction transaction)
 {
     return(transaction.TransactionTypeCode.IgnoreCaseEquals(Constants.TransactionTypes.LOAD) && transaction.QuantityEarned == 0);
 }
        /// <summary>
        /// Advances the time sheet enumerator so that the current time sheet's punchOutTime > transactionDate.
        /// If no more time sheets exist that meet that criteria null is returned.
        /// </summary>
        /// <param name="enumerator"></param>
        /// <param name="transaction"></param>
        /// <returns></returns>
        internal static (EfficiencyTimeSheet TimeSheet, EfficiencyTimeSheet PreviousTimeSheet) AdvanceTimeSheet([NotNull] SafeEnumerator <EfficiencyTimeSheet> enumerator, Transaction transaction)
        {
            var transactionDate = transaction.TransactionDate;

            if (enumerator.Current?.PunchOutTime == null || enumerator.Current?.PunchOutTime >= transactionDate)
            {
                return(enumerator.Current, enumerator.Previous);
            }

            while (enumerator.MoveNext())
            {
                var current = enumerator.Current;

                if (current == null)
                {
                    continue;
                }

                if (current.PunchInTime > current.PunchOutTime)
                {
                    throw new InvalidOperationException(
                              $"Invalid TimeSheet. {nameof(EfficiencyTimeSheet.PunchInTime)} > {nameof(EfficiencyTimeSheet.PunchOutTime)}");
                }

                if (enumerator.Previous?.PunchInTime > current?.PunchInTime)
                {
                    throw new ArgumentException("TimeSheets must be ordered by PunchInTime",
                                                nameof(enumerator));
                }

                // We found the next time sheet. Break out.
                if (current.PunchOutTime == null || current.PunchOutTime >= transactionDate)
                {
                    break;
                }
            }

            return(enumerator.Current, enumerator.Previous);
        }