diff --git a/src/main/java/de/zendric/app/xpensely_server/model/DTO/RecentHistoryEntryDto.java b/src/main/java/de/zendric/app/xpensely_server/model/DTO/RecentHistoryEntryDto.java new file mode 100644 index 0000000..6faccfd --- /dev/null +++ b/src/main/java/de/zendric/app/xpensely_server/model/DTO/RecentHistoryEntryDto.java @@ -0,0 +1,12 @@ +package de.zendric.app.xpensely_server.model.DTO; + +import java.time.LocalDateTime; + +public record RecentHistoryEntryDto( + String type, + String actorUsername, + String expenseTitle, + LocalDateTime timestamp, + Long listId, + String listName) { +} diff --git a/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseHistoryRepository.java b/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseHistoryRepository.java index d6f283e..e6c5f6d 100644 --- a/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseHistoryRepository.java +++ b/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseHistoryRepository.java @@ -13,6 +13,9 @@ public interface ExpenseHistoryRepository extends JpaRepository findByExpenseListIdOrderByTimestampDescIdDesc(Long expenseListId, Pageable pageable); + Page findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc( + Long ownerId, Long sharedWithId, Pageable pageable); + @Query("select h.expenseId from ExpenseHistoryEntry h") List findAllExpenseIds(); diff --git a/src/main/java/de/zendric/app/xpensely_server/services/HistoryService.java b/src/main/java/de/zendric/app/xpensely_server/services/HistoryService.java index 8cd65ec..6ea8ebf 100644 --- a/src/main/java/de/zendric/app/xpensely_server/services/HistoryService.java +++ b/src/main/java/de/zendric/app/xpensely_server/services/HistoryService.java @@ -17,6 +17,7 @@ import tools.jackson.databind.ObjectMapper; import de.zendric.app.xpensely_server.model.AppUser; import de.zendric.app.xpensely_server.model.DTO.HistoryEntryDto; import de.zendric.app.xpensely_server.model.DTO.HistoryPageDto; +import de.zendric.app.xpensely_server.model.DTO.RecentHistoryEntryDto; import de.zendric.app.xpensely_server.model.Expense; import de.zendric.app.xpensely_server.model.ExpenseHistoryEntry; import de.zendric.app.xpensely_server.model.ExpenseList; @@ -104,6 +105,23 @@ public class HistoryService { return new HistoryPageDto(entries, result.hasNext()); } + @Transactional(readOnly = true) + public List getRecentForUser(AppUser user, int limit) { + int cappedLimit = Math.min(Math.max(limit, 1), 20); + Page result = historyRepository + .findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc( + user.getId(), user.getId(), PageRequest.of(0, cappedLimit)); + return result.getContent().stream() + .map(e -> new RecentHistoryEntryDto( + e.getType().name(), + e.getActor() == null ? null : e.getActor().getUsername(), + e.getExpenseTitle(), + e.getTimestamp(), + e.getExpenseList().getId(), + e.getExpenseList().getName())) + .toList(); + } + private HistoryEntryDto toDto(ExpenseHistoryEntry entry) { List changes = List.of(); if (entry.getChanges() != null) { diff --git a/src/test/java/de/zendric/app/xpensely_Server/services/HistoryServiceTest.java b/src/test/java/de/zendric/app/xpensely_Server/services/HistoryServiceTest.java index d1aca0f..7301a35 100644 --- a/src/test/java/de/zendric/app/xpensely_Server/services/HistoryServiceTest.java +++ b/src/test/java/de/zendric/app/xpensely_Server/services/HistoryServiceTest.java @@ -18,6 +18,7 @@ import tools.jackson.databind.ObjectMapper; import de.zendric.app.xpensely_server.model.AppUser; import de.zendric.app.xpensely_server.model.DTO.HistoryPageDto; +import de.zendric.app.xpensely_server.model.DTO.RecentHistoryEntryDto; import de.zendric.app.xpensely_server.model.Expense; import de.zendric.app.xpensely_server.model.ExpenseHistoryEntry; import de.zendric.app.xpensely_server.model.ExpenseList; @@ -206,4 +207,75 @@ class HistoryServiceTest { assertThat(page.entries().get(0).changes()).isEmpty(); } + + private ExpenseHistoryEntry historyEntry(Long id, ExpenseList list, AppUser actor, + HistoryEntryType type, String title, java.time.LocalDateTime timestamp) { + ExpenseHistoryEntry e = new ExpenseHistoryEntry(); + e.setId(id); + e.setExpenseList(list); + e.setActor(actor); + e.setType(type); + e.setExpenseTitle(title); + e.setTimestamp(timestamp); + return e; + } + + @Test + void getRecentForUser_mapsEntriesWithListIdAndName() { + AppUser alice = user(1L, "alice"); + ExpenseList list = new ExpenseList(); + list.setId(10L); + list.setName("Trip to Rome"); + ExpenseHistoryEntry entry = historyEntry(100L, list, alice, + HistoryEntryType.UPDATED, "Groceries", java.time.LocalDateTime.of(2026, 7, 6, 12, 0)); + + when(historyRepository + .findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc( + eq(1L), eq(1L), any(Pageable.class))) + .thenReturn(new PageImpl<>(List.of(entry))); + + List result = service.getRecentForUser(alice, 4); + + assertThat(result).containsExactly(new RecentHistoryEntryDto( + "UPDATED", "alice", "Groceries", + java.time.LocalDateTime.of(2026, 7, 6, 12, 0), 10L, "Trip to Rome")); + } + + @Test + void getRecentForUser_nullActor_mapsToNullUsername() { + AppUser alice = user(1L, "alice"); + ExpenseList list = new ExpenseList(); + list.setId(10L); + list.setName("Trip to Rome"); + ExpenseHistoryEntry entry = historyEntry(100L, list, null, + HistoryEntryType.CREATED, "Taxi", java.time.LocalDateTime.of(2026, 7, 6, 12, 0)); + + when(historyRepository + .findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc( + eq(1L), eq(1L), any(Pageable.class))) + .thenReturn(new PageImpl<>(List.of(entry))); + + List result = service.getRecentForUser(alice, 4); + + assertThat(result.get(0).actorUsername()).isNull(); + } + + @Test + void getRecentForUser_clampsLimitBetween1And20() { + AppUser alice = user(1L, "alice"); + when(historyRepository + .findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc( + eq(1L), eq(1L), any(Pageable.class))) + .thenReturn(new PageImpl<>(List.of())); + + service.getRecentForUser(alice, 500); + verify(historyRepository) + .findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc( + 1L, 1L, PageRequest.of(0, 20)); + + service.getRecentForUser(alice, 0); + verify(historyRepository) + .findByExpenseListOwnerIdOrExpenseListSharedWithIdOrderByTimestampDescIdDesc( + 1L, 1L, PageRequest.of(0, 1)); + } }