Move deleted projects to the trash for 30 days

Java · Spring Boot · beginner · modification

Deleting a project now moves it to the trash instead of dropping the row, so a member who deletes the wrong project has 30 days to bring it back. `DELETE` sets `deletedAt`; the list and get endpoints filter trashed projects out; the new `POST .../restore` endpoint clears `deletedAt` while the project is still inside its 30 days; and `TrashPurgeJob` deletes whatever has been in the trash for 30 days or more every night at 03:00 UTC. Trashing and restoring are each a single conditional `UPDATE`, so a double-clicked delete gets one 204 and one 404. Time comes from the injected `Clock`, like `createdAt` already does.

Project management app on Spring Boot 3.3 (Java 21), Spring Data JPA with Hibernate 6.5 on PostgreSQL 16; Hibernate maps `Instant` to `timestamptz`. Existing and unchanged: the `Clock` bean (`Clock.systemUTC()`), `@EnableScheduling` on a config class (the app already runs other `@Scheduled` jobs, on a single instance), `ProjectResponse` (`id`, `name`, `createdAt`), `CreateProjectRequest`, the `workspaceAccess.isMember` check that the class-level `@PreAuthorize` applies to every endpoint of the controller (any member may delete and restore), and `ProjectNotFoundException`, annotated `@ResponseStatus(NOT_FOUND)`. `Project` has no JPA associations; tasks and comments reference `projects.id` with `ON DELETE CASCADE` in the schema, which the old `DELETE` already relied on. Project names need not be unique. Nothing outside `ProjectService` uses `ProjectRepository`, and nothing outside `ProjectController` calls `ProjectService.delete`, so the methods this PR renames have no other callers. The task and comment endpoints (`/projects/{id}/tasks`, `/tasks/{id}/comments`) are out of scope for this PR. The nullable `deleted_at timestamptz` column is added by a migration that ships separately and is already applied; a workspace has at most a few thousand projects.

Requirements

Files touched

--- src/main/java/com/acme/projects/project/Project.java
     private Instant createdAt;
 
+    private Instant deletedAt;
+
     protected Project() {}
 
     public String getName() { return name; }
     public Instant getCreatedAt() { return createdAt; }
+    public Instant getDeletedAt() { return deletedAt; }
 }
 
--- src/main/java/com/acme/projects/project/ProjectRepository.java
 package com.acme.projects.project;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 
 public interface ProjectRepository extends JpaRepository<Project, Long> {
 
-    List<Project> findByWorkspaceIdOrderByNameAsc(Long workspaceId);
+    List<Project> findByWorkspaceIdAndDeletedAtIsNullOrderByNameAsc(Long workspaceId);
 
-    Optional<Project> findByIdAndWorkspaceId(Long id, Long workspaceId);
+    Optional<Project> findByIdAndWorkspaceIdAndDeletedAtIsNull(Long id, Long workspaceId);

Review this PR

Java practice