fix: allow editing quick-add stub expenses (amount 0, no category)
Build and Deploy Spring Boot Server / build (push) Successful in 3m36s

Quick Add deliberately creates stub expenses with amount 0 and no
category (ExpenseInput allows both), but ExpenseChangeRequest required
amount >= 0.01 and a non-blank category, so every edit of a stub that
did not simultaneously fill in both fields was rejected with 400.
Update validation now accepts the same states create can produce, and
a blank category is normalized to null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 18:52:42 +02:00
parent ac0118d435
commit b58008c8bc
2 changed files with 54 additions and 3 deletions
@@ -24,8 +24,10 @@ public class ExpenseChangeRequest {
@NotBlank(message = "Owner name is required")
private String ownerName;
// Same bounds as ExpenseInput: Quick Add creates stub expenses with amount 0
// and no category, and those stubs must remain editable.
@NotNull(message = "Amount is required")
@DecimalMin(value = "0.01", message = "Amount must be greater than zero")
@DecimalMin(value = "0.00", message = "Amount must not be negative")
private Double amount;
private Double personalUseAmount;
@@ -34,7 +36,6 @@ public class ExpenseChangeRequest {
@NotNull(message = "Date is required")
private LocalDate date;
@NotBlank(message = "Category is required")
private String category;
public Expense convertToExpense(Long userId, ExpenseList expenseList) {
@@ -51,7 +52,9 @@ public class ExpenseChangeRequest {
expense.setId(id);
expense.setOwner(appUser);
expense.setTitle(title);
expense.setCategory(category);
// The edit dialog sends "" for a missing category; store null so the
// stub state stays uniform and history diffs don't report null -> "".
expense.setCategory(category == null || category.isBlank() ? null : category);
return expense;
}
@@ -53,6 +53,54 @@ class ExpenseListControllerTest {
.andExpect(jsonPath("$.title").exists());
}
// Quick Add creates stub expenses (amount 0, no category); updating such a
// stub must be allowed so the user can fill in the details afterwards.
@Test
void updateExpense_quickAddStub_zeroAmountAndNoCategory_returns200() throws Exception {
AppUser owner = new AppUser(); owner.setId(1L); owner.setUsername("alice");
ExpenseList list = new ExpenseList(); list.setId(1L); list.setOwner(owner);
when(expenseListService.findById(1L)).thenReturn(Optional.of(list));
when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(owner);
when(expenseListService.updateExpense(eq(1L), any(Expense.class), eq(owner)))
.thenAnswer(inv -> inv.getArgument(1));
mockMvc.perform(put("/api/expenselist/1/update")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"id\":42,\"title\":\"Groceries\",\"ownerName\":\"alice\",\"amount\":0.0,"
+ "\"personalUseAmount\":0.0,\"otherPersonAmount\":0.0,\"date\":\"2026-07-06\"}"))
.andExpect(status().isOk());
}
@Test
void updateExpense_blankCategory_isStoredAsNull() throws Exception {
AppUser owner = new AppUser(); owner.setId(1L); owner.setUsername("alice");
ExpenseList list = new ExpenseList(); list.setId(1L); list.setOwner(owner);
when(expenseListService.findById(1L)).thenReturn(Optional.of(list));
when(authenticatedUserResolver.resolveCurrentUser(any())).thenReturn(owner);
when(expenseListService.updateExpense(eq(1L), any(Expense.class), eq(owner)))
.thenAnswer(inv -> inv.getArgument(1));
mockMvc.perform(put("/api/expenselist/1/update")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"id\":42,\"title\":\"Groceries\",\"ownerName\":\"alice\",\"amount\":0.0,"
+ "\"personalUseAmount\":0.0,\"otherPersonAmount\":0.0,\"date\":\"2026-07-06\",\"category\":\"\"}"))
.andExpect(status().isOk());
org.mockito.ArgumentCaptor<Expense> captor = org.mockito.ArgumentCaptor.forClass(Expense.class);
org.mockito.Mockito.verify(expenseListService).updateExpense(eq(1L), captor.capture(), eq(owner));
org.assertj.core.api.Assertions.assertThat(captor.getValue().getCategory()).isNull();
}
@Test
void updateExpense_negativeAmount_returns400() throws Exception {
mockMvc.perform(put("/api/expenselist/1/update")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"id\":42,\"title\":\"Groceries\",\"ownerName\":\"alice\",\"amount\":-5.0,"
+ "\"personalUseAmount\":0.0,\"otherPersonAmount\":0.0,\"date\":\"2026-07-06\",\"category\":\"Food\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.amount").exists());
}
@Test
void addExpense_negativeAmount_returns400() throws Exception {
mockMvc.perform(post("/api/expenselist/1/add")