3636@ Slf4j
3737public class IssueService {
3838 private static final String ISSUE_TYPE_EPIC_NAME = "Epic" ;
39+ private static final double DEFAULT_POINTS = 3.0 ;
3940
4041 private final GitHubClient gitHubClient ;
4142 private final Mapper mapper ;
@@ -241,8 +242,9 @@ private Set<Issue> saveIssues(Set<Issue> issues) {
241242 */
242243 public Set <IssueResponse > getIssuesByReleaseId (String releaseId ) throws ReleaseNotFoundException {
243244 Release release = releaseService .checkIfReleaseExists (releaseId );
244- Set <Issue > rootIssues = issueRepository .findIssuesByReleaseId (release .getId ());
245- return buildIssueResponseTree (rootIssues );
245+ Set <Issue > allIssues = issueRepository .findIssuesByReleaseId (release .getId ());
246+ Set <Issue > rootIssues = filterRootIssues (allIssues );
247+ return buildIssueResponseTree (rootIssues );
246248 }
247249
248250 /**
@@ -253,7 +255,8 @@ public Set<IssueResponse> getIssuesByReleaseId(String releaseId) throws ReleaseN
253255 */
254256 public Set <IssueResponse > getIssuesByMilestoneId (String milestoneId ) throws MilestoneNotFoundException {
255257 Milestone milestone = milestoneService .checkIfMilestoneExists (milestoneId );
256- Set <Issue > rootIssues = issueRepository .findDistinctByMilestoneId (milestone .getId ());
258+ Set <Issue > allIssues = issueRepository .findDistinctByMilestoneId (milestone .getId ());
259+ Set <Issue > rootIssues = filterRootIssues (allIssues );
257260 return buildIssueResponseTree (rootIssues );
258261 }
259262
@@ -263,7 +266,24 @@ public Set<IssueResponse> getIssuesByMilestoneId(String milestoneId) throws Mile
263266 */
264267 public Set <IssueResponse > getFutureEpicIssues () {
265268 Set <Issue > futureEpicIssues = issueRepository .findIssuesByIssueTypeNameAndMilestoneIsNull (ISSUE_TYPE_EPIC_NAME );
266- return buildIssueResponseTree (futureEpicIssues );
269+ return buildIssueResponseTreeWithoutFiltering (futureEpicIssues );
270+ }
271+
272+ /**
273+ * Filters out issues that are sub-issues of other issues in the set.
274+ * @param issues the set of all issues
275+ * @return a set of root issues (issues that are not sub-issues of other issues in the set)
276+ */
277+ private Set <Issue > filterRootIssues (Set <Issue > issues ) {
278+ Set <String > allSubIssueIds = issues .stream ()
279+ .filter (issue -> issue .getSubIssues () != null )
280+ .flatMap (issue -> issue .getSubIssues ().stream ())
281+ .map (Issue ::getId )
282+ .collect (Collectors .toSet ());
283+
284+ return issues .stream ()
285+ .filter (issue -> !allSubIssueIds .contains (issue .getId ()))
286+ .collect (Collectors .toSet ());
267287 }
268288
269289 /**
@@ -272,6 +292,20 @@ public Set<IssueResponse> getFutureEpicIssues() {
272292 * @return a set of IssueResponse objects representing the root issues and their sub-issues, with labels included
273293 */
274294 private Set <IssueResponse > buildIssueResponseTree (Set <Issue > rootIssues ) {
295+ Set <String > allIds = collectAllIssueIdsRecursively (rootIssues );
296+ Map <String , Set <LabelResponse >> labelsMap = fetchLabelsForIssueIds (allIds );
297+ return rootIssues .stream ()
298+ .map (issue -> mapIssueTreeWithLabels (issue , labelsMap ))
299+ .filter (this ::hasRelevantLabelsRecursively )
300+ .collect (Collectors .toSet ());
301+ }
302+
303+ /**
304+ * Builds a tree of IssueResponse objects without filtering by labels.
305+ * @param rootIssues the set of root issues to build the tree from
306+ * @return a set of IssueResponse objects representing the root issues and their sub-issues, with labels included
307+ */
308+ private Set <IssueResponse > buildIssueResponseTreeWithoutFiltering (Set <Issue > rootIssues ) {
275309 Set <String > allIds = collectAllIssueIdsRecursively (rootIssues );
276310 Map <String , Set <LabelResponse >> labelsMap = fetchLabelsForIssueIds (allIds );
277311 return rootIssues .stream ()
@@ -309,6 +343,7 @@ private Stream<String> flattenIssueIds(Issue issue) {
309343 private Map <String , Set <LabelResponse >> fetchLabelsForIssueIds (Set <String > issueIds ) {
310344 Set <IssueLabel > labels = issueLabelRepository .findAllByIssue_IdIn (new ArrayList <>(issueIds ));
311345 return labels .stream ()
346+ .filter (l -> labelService .isLabelIncluded (l .getLabel ()))
312347 .collect (Collectors .groupingBy (
313348 l -> l .getIssue ().getId (),
314349 Collectors .mapping (l -> mapper .toDTO (l .getLabel (), LabelResponse .class ), Collectors .toSet ())));
@@ -328,6 +363,10 @@ private IssueResponse mapIssueTreeWithLabels(Issue issue, Map<String, Set<LabelR
328363
329364 response .setLabels (labelsMap .getOrDefault (issue .getId (), Set .of ()));
330365 response .setSubIssues (mapSubIssuesToResponses (issue , labelsMap ));
366+
367+ double totalPoints = calculateTotalPoints (issue , response );
368+ response .setPoints (totalPoints );
369+
331370 return response ;
332371 }
333372
@@ -374,12 +413,49 @@ private Set<IssueResponse> mapSubIssuesToResponses(Issue issue, Map<String, Set<
374413 if (issue .getSubIssues () != null && !issue .getSubIssues ().isEmpty ()) {
375414 return issue .getSubIssues ().stream ()
376415 .map (sub -> mapIssueTreeWithLabels (sub , labelsMap ))
416+ .filter (this ::hasRelevantLabelsRecursively )
377417 .collect (Collectors .toSet ());
378418 } else {
379419 return Set .of ();
380420 }
381421 }
382422
423+ /**
424+ * Checks if an issue or any of its sub-issues have relevant labels.
425+ * @param issueResponse the issue response to check
426+ * @return true if the issue or any of its sub-issues have at least one relevant label
427+ */
428+ private boolean hasRelevantLabelsRecursively (IssueResponse issueResponse ) {
429+ if (issueResponse .getLabels () != null && !issueResponse .getLabels ().isEmpty ()) {
430+ return true ;
431+ }
432+ if (issueResponse .getSubIssues () != null
433+ && !issueResponse .getSubIssues ().isEmpty ()) {
434+ return issueResponse .getSubIssues ().stream ().anyMatch (this ::hasRelevantLabelsRecursively );
435+ }
436+ return false ;
437+ }
438+
439+ /**
440+ * Calculates the total points for an issue, including its own points and all sub-issue points.
441+ * Uses DEFAULT_POINTS (3.0) for issues without assigned points.
442+ * @param issue the issue entity
443+ * @param response the issue response with mapped sub-issues
444+ * @return the total points for the issue and all its sub-issues
445+ */
446+ private double calculateTotalPoints (Issue issue , IssueResponse response ) {
447+ double issuePoints = issue .getPoints () != null ? issue .getPoints () : DEFAULT_POINTS ;
448+
449+ if (response .getSubIssues () != null && !response .getSubIssues ().isEmpty ()) {
450+ double subIssuesPoints = response .getSubIssues ().stream ()
451+ .mapToDouble (IssueResponse ::getPoints )
452+ .sum ();
453+ return issuePoints + subIssuesPoints ;
454+ }
455+
456+ return issuePoints ;
457+ }
458+
383459 /**
384460 * Get all issues from the database
385461 * @return a map of issue id to issue
0 commit comments