private void AllocateTimeToWorkItems(TimeEntry timeEntry)
        {
            var referencedStoryIds = MavenlinkHelper.GetJiraStoryIds(timeEntry.NotesOverride);

            if (referencedStoryIds.Count == 0)
            {
                return;
            }

            decimal timePerStory = (referencedStoryIds.Count == 0)
                                ? (decimal)timeEntry.DurationMinutesOverride
                                : (decimal)((decimal)timeEntry.DurationMinutesOverride / referencedStoryIds.Count);

            var workItemsInLocalCache = Database.WorkItems
                                        .Where(x => referencedStoryIds.Contains(x.StoryNumber))
                                        .OrderBy(x => x.StoryNumber)
                                        .ToList();

            foreach (var workItem in workItemsInLocalCache)
            {
                timeEntry.WorkItems.Add(
                    new TimeEntryWorkItemAllocation {
                    TimeEntry       = timeEntry,
                    WorkItem        = workItem,
                    DurationMinutes = timePerStory
                }
                    );
            }
        }
Exemple #2
0
        public async Task <List <TimeEntryValidationResult> > Validate(Release release, List <TimeEntry> timeEntries)
        {
            var jiraStoriesInRelease = (await JiraApi.GetStoriesInReleaseAsync(release.ReleaseNumber));

            var results = new List <TimeEntryValidationResult>();

            foreach (var timeEntry in timeEntries)
            {
                var errors   = new List <string>();
                var warnings = new List <string>();

                var isPlanned          = (MavenlinkHelper.GetBillingClassification(timeEntry.TaskTitleOverride) == BillingClassificationEnum.Planned);
                var referencedStoryIds = MavenlinkHelper.GetJiraStoryIds(timeEntry.NotesOverride);

                if (referencedStoryIds.Count <= 1 && timeEntry.DurationMinutesOverride < 15)
                {
                    warnings.Add($"Minimum billing increment is 15 minutes");
                }
                else if (referencedStoryIds.Count > 1)
                {
                    var timePerStory = (timeEntry.DurationMinutesOverride / referencedStoryIds.Count);

                    if (timePerStory < 15m)
                    {
                        warnings.Add($"Each Jira story would receive {timePerStory} minutes of elapsed time, less than the 15-min minimum billing increment.");
                    }
                }

                foreach (var jiraStoryId in referencedStoryIds)
                {
                    var jiraStory = jiraStoriesInRelease
                                    .Where(x => x.Id == jiraStoryId)
                                    .FirstOrDefault();

                    // we're loading all of the stories in the release in a single load for performance, but if time is
                    // billed to a story NOT in the release we still need to load it as a 1-off.
                    if (jiraStory == null)
                    {
                        try {
                            jiraStory = await JiraApi.GetStoryAsync(jiraStoryId);
                        }
                        catch (NotFoundException) {
                            errors.Add($"Jira ID '{jiraStoryId}' was not found in Jira");
                            continue;
                        }
                    }

                    // Billing time to a story not associated with the release is a warning; the time will be counted towards
                    // the "undelivered" category and will not affect the metrics
                    if (!jiraStory.FixVersions.Contains(release.ReleaseNumber))
                    {
                        warnings.Add($"Jira {jiraStory.IssueType} '{jiraStoryId}' is not tagged to release {release.ReleaseNumber} and has status '{jiraStory.Status}'. Time will be counted towards 'undelivered'.");
                    }

                    var workItemType     = JiraHelper.GetWorkItemType(jiraStory);
                    var isEpic           = (workItemType == WorkItemTypeEnum.Epic);
                    var isDeclined       = jiraStory.Status.EqualsIgnoringCase("Declined");
                    var isContingency    = (workItemType == WorkItemTypeEnum.Contingency);
                    var isFeatureRequest = (workItemType == WorkItemTypeEnum.FeatureRequest);

                    if (isFeatureRequest)
                    {
                        warnings.Add($"'{jiraStoryId}' is a Feature Request; Feature Requests should not be billed as development, they should either be billed to a specific analysis ticket or to Unplanned Analysis.");
                    }

                    if (isDeclined)
                    {
                        warnings.Add($"Jira {jiraStory.IssueType} '{jiraStoryId}' is marked as 'DECLINED'. Time will be tracked towards 'undelivered'.");
                    }

                    if (isEpic && isPlanned)
                    {
                        warnings.Add($"'{jiraStoryId}' is an Epic; epics should generally have Overhead or Unplanned time instead of Planned.");
                    }

                    if (isContingency)
                    {
                        errors.Add($"'{jiraStoryId}' is a Contingency; Contingency cases should not have time billed to them. The time (and contingency points) should be moved to an actual feature.");
                    }
                }

                if (errors.Any() || warnings.Any())
                {
                    results.Add(
                        new TimeEntryValidationResult(timeEntry.Id, errors, warnings)
                        );
                }
            }

            return(results);
        }