Skip to content

Commit 5ebec52

Browse files
authored
Merge pull request #1118 from IT-Academy-BCN/feature/190-2/point-history-retrival-logic
[BE] PR 2/3: Point History Retrieval Logic (#216)
2 parents 54c2c81 + ced6d51 commit 5ebec52

File tree

8 files changed

+308
-26
lines changed

8 files changed

+308
-26
lines changed

CHANGELOG.md

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,25 @@
33
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
44
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

6+
## [UNRELEASED] 2026-02-19
7+
### Added
8+
- PointsHistoryResponseDto: New DTO to encapsulate the response structure, including total points and the collection of user activities.
9+
- PointHistoryEntryDto: New DTO for individual score entries, including points and formatted date strings.
10+
- UserScoreService: Core interface definition for gamification point-related business operations.
11+
- UserScoreServiceImpl: Implementation of the points history logic, featuring reactive stream processing and score aggregation.
12+
- UserScoreServiceImplTest: Comprehensive unit test suite for PointsServiceImpl covering all business requirements.
13+
614
### [itachallenge-user-3.2.3-RELEASE] - 2026-02-18
715

816
### Removed
917
- Removed deprecated legacy Bookmarks routes previously served by BookmarkLegacyController. (GitHub Task [#186], PR [#1106])
10-
-
1118
## [UNRELEASED]
1219

1320
### Added
1421
- New MongoDB collection `user_score_history` with compound indexing for efficient retrieval for gamification tracking.
1522
- Reactive Repository for user score transactions.
1623

24+
## [Unreleased]
1725
### [itachallenge-user-3.2.2-RELEASE] - 2026-02-23
1826

1927
### Removed
@@ -67,12 +75,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6775
### Added
6876
- Submission domain bootstrap (documents, repository, service layer and DTOs).
6977
- New `SubmissionController` exposing read endpoints for user submissions.
70-
78+
7179
### [itachallenge-challenge-3.0.4-RELEASE] - 2025-12-17
7280

7381
### Fixed
7482
- Updated the favorites integration path to point to the new `FavoriteController` of the `user` micro.
75-
(No changes to the public `challenge` API. Bookmarks remains the same.)
83+
(No changes to the public `challenge` API. Bookmarks remains the same.)
7684

7785
### [itachallenge-user-3.1.3-RELEASE] - 2025-12-12
7886

@@ -110,11 +118,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
110118

111119
### Breaking Change
112120
- API Contract: Replaced the `status` field with action in UserSolutionRequestDto.
113-
Supported actions: SAVE, GIVE_UP, SUBMIT. (Taiga [#871], PR [#1043])
121+
Supported actions: SAVE, GIVE_UP, SUBMIT. (Taiga [#871], PR [#1043])
114122
- New SolutionAction enum : Action-based submission workflow: Implemented business logic to map user actions to internal solution statuses:
115-
- SAVE → IN_PROGRESS
116-
- GIVE_UP → SUBMITTED_UNCOMPLETED
117-
- SUBMIT → SUBMITTED_COMPLETED
123+
- SAVE → IN_PROGRESS
124+
- GIVE_UP → SUBMITTED_UNCOMPLETED
125+
- SUBMIT → SUBMITTED_COMPLETED
118126
- Updated OpenAPI documentation.
119127

120128
## [Unreleased]
@@ -185,10 +193,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
185193
### Added
186194
- Foundation for managing user favorites in a dedicated collection.
187195
- Favorite domain & persistence
188-
- FavoriteDocument & FavoriteDocumentTest
196+
- FavoriteDocument & FavoriteDocumentTest
189197
- FavoriteRepository (ReactiveMongoRepository<FavoriteDocument, UUID>) ; Favorites collection starts empty.
190198
- FavoriteResponseDto & FavoriteResponseDtoTest
191-
199+
192200

193201
### Changed
194202
- UserServiceImpl
@@ -226,20 +234,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
226234
- Now returns 503 Service Unavailable or 504 Gateway Timeout instead of a generic 500.
227235
- Introduced `GithubUnavailableException` to encapsulate timeout and unavailability causes.
228236
- Updated `UserGlobalExceptionHandler` to map these cases accordingly.
229-
-
237+
-
230238
### [itachallenge-user-3.0.0-RELEASE] - 2025-10-09
231239

232240
### Changed
233-
- Before: SolutionStatus enum only had two options `ENDED` and `IN_PROGRESS`, solutions with status `ENDED` could be modified,
241+
- Before: SolutionStatus enum only had two options `ENDED` and `IN_PROGRESS`, solutions with status `ENDED` could be modified,
234242
leading to ambiguity in submission state.
235243
- After: SolutionStatus enum options can be marked as `IN_PROGRESS`, `SUBMITTED_COMPLETE` or `SUBMITTED_INCOMPLETE`.
236244
Solutions marked as `SUBMITTED_COMPLETE` or `SUBMITTED_INCOMPLETE` now throw `UnmodifiableSolutionException`
237245
when updated. Only `IN_PROGRESS` solutions remain editable.
238246
- This changes prevents accidental overwrites of finalized submissions.
239247
- this changes will break the communication between back and frontend, it is required to adjust the type of answer that can be submitted
240-
by users ENDED is replaced with SUBMITTED_COMPLETE and SUBMITTED_INCOMPLETE has to be added to better reflect the status
248+
by users ENDED is replaced with SUBMITTED_COMPLETE and SUBMITTED_INCOMPLETE has to be added to better reflect the status
241249
in which challenges can be set (Taiga user story [#703])
242-
250+
243251
### [itachallenge-user-2.1.0-RELEASE] - 2025-10-03
244252

245253
### Added
@@ -256,8 +264,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
256264

257265
### Changed
258266
- Added github username validation to user microservice upon creation of a new user (Taiga [#650], PR [#960])
259-
- Before: POST /users/create accepted any githubUsername value and returned success (201/ok) even if that wasn't a GitHub username.
260-
- After: POST /users/create now calls the GitHub API and rejects the request when the GitHub username does not exist.
267+
- Before: POST /users/create accepted any githubUsername value and returned success (201/ok) even if that wasn't a GitHub username.
268+
- After: POST /users/create now calls the GitHub API and rejects the request when the GitHub username does not exist.
261269
- Clients that previously relied on creating users with invalid GitHub usernames will now get errors for some requests that used to succeed.
262270

263271
### [itachallenge-githubcore-1.0.0-RELEASE] - 2025-09-11
@@ -369,15 +377,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
369377

370378
### Added
371379
- Add validation to verify that the provided user ID exists for the getAllSolutionsByUser endpoint. (Taiga [#437], PR [#889])
372-
- FavoriteController and extracted endpoints from ChallengeController. (Taiga [#418], PR [#886])
373-
- POST endpoint in Auth microservice to allow user role change at runtime (Taiga [#404], PR [#882])
374-
- Hardcoded GET endpoint to return all of a user’s challenge solutions (Taiga [#435], PR [#884])
375-
- JWT authentication to logout endpoint in User microservice (Taiga [#399], PR [#864])
376-
- Error handling for malformed tag UUIDs and duplicate UUID detection in TagServiceImpl.getValidatedTags (Taiga [#355], PR [#872])
377-
- Tag validation in the addChallenge endpoint (Taiga [#354], PR [#866])
380+
- FavoriteController and extracted endpoints from ChallengeController. (Taiga [#418], PR [#886])
381+
- POST endpoint in Auth microservice to allow user role change at runtime (Taiga [#404], PR [#882])
382+
- Hardcoded GET endpoint to return all of a user’s challenge solutions (Taiga [#435], PR [#884])
383+
- JWT authentication to logout endpoint in User microservice (Taiga [#399], PR [#864])
384+
- Error handling for malformed tag UUIDs and duplicate UUID detection in TagServiceImpl.getValidatedTags (Taiga [#355], PR [#872])
385+
- Tag validation in the addChallenge endpoint (Taiga [#354], PR [#866])
378386
- GET endpoint for retrieving resources from a challenge (Taiga [#281], PR [#852])
379-
- GET endpoint to retrieve all user's bookmarked challenges (Taiga [#255], PR [#849])
380-
- DELETE endpoint in Challenge microservice to unbookmark a challenge (Taiga [#205], PR [#843])
387+
- GET endpoint to retrieve all user's bookmarked challenges (Taiga [#255], PR [#849])
388+
- DELETE endpoint in Challenge microservice to unbookmark a challenge (Taiga [#205], PR [#843])
381389
- POST endpoint in Challenge microservice to bookmark a challenge (Taiga [#183], PR [#829])
382390
- POST endpoint in User microservice to bookmark a challenge (Taiga [#183], PR [#827])
383391
- DELETE endpoint for user's bookmarks in User microservice (Taiga [#205], PR [#831])
@@ -389,7 +397,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
389397
- Replaced Hardcoded GET endpoint to return all of a user’s challenge solutions with actual solutions (Taiga [#406], PR [#887])
390398
- POST endpoint for adding new challenges in Challenge microservice (PR #763)
391399
- Improved filtering at `/GET Challenges`: added DTO for filters (language, level, tags), refactored `filterByLanguage()`, `filterByLevel()`, and `filterByTags()` methods, and added `GET /allTags` endpoint (PR #180 & PR #185)
392-
- Modify method to increase times solved counter (Taiga [#415], PR [#885])
400+
- Modify method to increase times solved counter (Taiga [#415], PR [#885])
393401

394402
### Removed
395403
- Removed `score` attribute from `UserSolution` DTOs and cleaned up all score/solution-related code in User microservice (PR #825, PR #725)
@@ -436,4 +444,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
436444
* First version
437445

438446
### [docker-compose-1.0] - 2023-11-30
439-
* First version
447+
* First version
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.itachallenge.challenge.dto.gamification;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.*;
5+
6+
@Getter
7+
@Setter
8+
@Builder
9+
@AllArgsConstructor
10+
@NoArgsConstructor
11+
public class PointHistoryEntryDto {
12+
@JsonProperty("date")
13+
private String createdAt;
14+
private int points;
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.itachallenge.challenge.dto.gamification;
2+
3+
import lombok.*;
4+
5+
import java.util.List;
6+
7+
@Getter
8+
@Setter
9+
@Builder
10+
@AllArgsConstructor
11+
@NoArgsConstructor
12+
public class PointsHistoryResponseDto {
13+
private String username;
14+
private int totalPoints;
15+
private List<PointHistoryEntryDto> history;
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.itachallenge.gamification.service;
2+
3+
import com.itachallenge.challenge.dto.gamification.PointsHistoryResponseDto;
4+
import reactor.core.publisher.Mono;
5+
6+
import java.util.UUID;
7+
8+
public interface UserScoreService {
9+
10+
Mono<PointsHistoryResponseDto> getUserPointsHistory(UUID userId);
11+
}
12+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.itachallenge.gamification.service;
2+
3+
import com.itachallenge.challenge.dto.gamification.PointHistoryEntryDto;
4+
import com.itachallenge.challenge.dto.gamification.PointsHistoryResponseDto;
5+
import com.itachallenge.gamification.document.UserScoreDocument;
6+
import com.itachallenge.gamification.repository.UserScoreRepository;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.stereotype.Service;
9+
import reactor.core.publisher.Mono;
10+
11+
import java.util.Comparator;
12+
import java.util.List;
13+
import java.util.Objects;
14+
import java.util.UUID;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
public class UserScoreServiceImpl implements UserScoreService {
19+
20+
private final UserScoreRepository userScoreRepository;
21+
22+
@Override
23+
public Mono<PointsHistoryResponseDto> getUserPointsHistory(UUID userId) {
24+
return userScoreRepository.findByUserIdOrderByCreatedAtDesc(userId)
25+
.collectList()
26+
.map(this::buildHistoryResponse);
27+
}
28+
29+
private PointsHistoryResponseDto buildHistoryResponse(List<UserScoreDocument> docs) {
30+
int totalPoints = docs.stream()
31+
.map(UserScoreDocument::getPointsEarned)
32+
.filter(Objects::nonNull)
33+
.mapToInt(Integer::intValue)
34+
.sum();
35+
36+
List<PointHistoryEntryDto> history = docs.stream()
37+
.sorted(Comparator.comparing(UserScoreDocument::getCreatedAt,
38+
Comparator.nullsLast(Comparator.naturalOrder())))
39+
.map(doc -> PointHistoryEntryDto.builder()
40+
.createdAt(doc.getCreatedAt() != null ? doc.getCreatedAt().toString() : "")
41+
.points(doc.getPointsEarned() != null ? doc.getPointsEarned() : 0)
42+
.build())
43+
.toList();
44+
45+
return PointsHistoryResponseDto.builder()
46+
.username(docs.isEmpty() ? "" : docs.getFirst().getUsername())
47+
.totalPoints(totalPoints)
48+
.history(history)
49+
.build();
50+
}
51+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.itachallenge.challenge.dto.gamification;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.boot.test.autoconfigure.json.JsonTest;
6+
import org.springframework.boot.test.json.JacksonTester;
7+
import org.springframework.boot.test.json.JsonContent;
8+
9+
import java.util.List;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
import static org.junit.jupiter.api.Assertions.fail;
13+
14+
@JsonTest
15+
class PointsHistoryResponseDtoTest {
16+
17+
@Autowired
18+
private JacksonTester<PointsHistoryResponseDto> json;
19+
20+
@Test
21+
void testSerializationContract() {
22+
try {
23+
PointHistoryEntryDto entry = PointHistoryEntryDto.builder()
24+
.createdAt("2011-01-11T11:11:11")
25+
.points(10)
26+
.build();
27+
28+
PointsHistoryResponseDto dto = PointsHistoryResponseDto.builder()
29+
.username("testUser")
30+
.totalPoints(10)
31+
.history(List.of(entry))
32+
.build();
33+
34+
JsonContent<PointsHistoryResponseDto> result = json.write(dto);
35+
36+
assertThat(result).hasJsonPathStringValue("@.username", "testUser");
37+
assertThat(result).hasJsonPathNumberValue("@.totalPoints", 10);
38+
assertThat(result).hasJsonPathStringValue("@.history[0].date", "2011-01-11T11:11:11");
39+
assertThat(result).hasJsonPathNumberValue("@.history[0].points", 10);
40+
assertThat(result).doesNotHaveJsonPath("@.history[0].createdAt");
41+
} catch (Exception e) {
42+
fail("Serialization failed: " + e.getMessage());
43+
}
44+
}
45+
}

itachallenge-challenge/src/test/java/com/itachallenge/gamification/repository/UserScoreRepositoryTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ class UserScoreRepositoryTest {
2222
@Test
2323
void givenExistingScores_whenFindByUsername_thenReturnsSortedByDescendingDates() {
2424
UUID userId = UUID.randomUUID();
25-
2625
UserScoreDocument score1 = UserScoreDocument.builder()
2726
.id(UUID.randomUUID())
2827
.userId(userId)

0 commit comments

Comments
 (0)